package com.bkjk.platform.jobcenter;

import com.bkjk.platform.jobcenter.discovery.util.InetUtils;
import com.xxl.job.core.executor.XxlJobExecutor;
import org.apache.commons.lang3.StringUtils;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;
import org.springframework.util.ClassUtils;

import javax.annotation.PostConstruct;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.*;
import java.security.CodeSource;
import java.security.ProtectionDomain;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.Enumeration;
import java.util.List;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.jar.JarFile;
import java.util.jar.Manifest;

@Configuration
@EnableConfigurationProperties({JobCenterProperties.class})
public class JobCenterAutoConfiguration {

    static class ApplicationHome {

        private final File source;

        private final File dir;

        /**
         * Create a new {@link ApplicationHome} instance.
         */
        public ApplicationHome() {
            this(null);
        }

        /**
         * Create a new {@link ApplicationHome} instance for the specified source class.
         * 
         * @param sourceClass the source class or {@code null}
         */
        public ApplicationHome(Class<?> sourceClass) {
            this.source = findSource(sourceClass != null ? sourceClass : getStartClass());
            this.dir = findHomeDir(this.source);
        }

        private File findDefaultHomeDir() {
            String userDir = System.getProperty("user.dir");
            return new File(org.springframework.util.StringUtils.hasLength(userDir) ? userDir : ".");
        }

        private File findHomeDir(File source) {
            File homeDir = source;
            homeDir = (homeDir != null ? homeDir : findDefaultHomeDir());
            if (homeDir.isFile()) {
                homeDir = homeDir.getParentFile();
            }
            homeDir = (homeDir.exists() ? homeDir : new File("."));
            return homeDir.getAbsoluteFile();
        }

        private File findSource(Class<?> sourceClass) {
            try {
                ProtectionDomain domain = (sourceClass != null ? sourceClass.getProtectionDomain() : null);
                CodeSource codeSource = (domain != null ? domain.getCodeSource() : null);
                URL location = (codeSource != null ? codeSource.getLocation() : null);
                File source = (location != null ? findSource(location) : null);
                if (source != null && source.exists() && !isUnitTest()) {
                    return source.getAbsoluteFile();
                }
                return null;
            } catch (Exception ex) {
                return null;
            }
        }

        private File findSource(URL location) throws IOException {
            URLConnection connection = location.openConnection();
            if (connection instanceof JarURLConnection) {
                return getRootJarFile(((JarURLConnection)connection).getJarFile());
            }
            return new File(location.getPath());
        }

        /**
         * Returns the application home directory.
         * 
         * @return the home directory (never {@code null})
         */
        public File getDir() {
            return this.dir;
        }

        private File getRootJarFile(JarFile jarFile) {
            String name = jarFile.getName();
            int separator = name.indexOf("!/");
            if (separator > 0) {
                name = name.substring(0, separator);
            }
            return new File(name);
        }

        /**
         * Returns the underlying source used to find the home directory. This is usually the jar file or a directory.
         * Can return {@code null} if the source cannot be determined.
         * 
         * @return the underlying source or {@code null}
         */
        public File getSource() {
            return this.source;
        }

        private Class<?> getStartClass() {
            try {
                ClassLoader classLoader = getClass().getClassLoader();
                return getStartClass(classLoader.getResources("META-INF/MANIFEST.MF"));
            } catch (Exception ex) {
                return null;
            }
        }

        private Class<?> getStartClass(Enumeration<URL> manifestResources) {
            while (manifestResources.hasMoreElements()) {
                try {
                    InputStream inputStream = manifestResources.nextElement().openStream();
                    try {
                        Manifest manifest = new Manifest(inputStream);
                        String startClass = manifest.getMainAttributes().getValue("Start-Class");
                        if (startClass != null) {
                            return ClassUtils.forName(startClass, getClass().getClassLoader());
                        }
                    } finally {
                        inputStream.close();
                    }
                } catch (Exception ex) {
                }
            }
            return null;
        }

        private boolean isUnitTest() {
            try {
                for (StackTraceElement element : Thread.currentThread().getStackTrace()) {
                    if (element.getClassName().startsWith("org.junit.")) {
                        return true;
                    }
                }
            } catch (Exception ex) {
            }
            return false;
        }

        @Override
        public String toString() {
            return getDir().toString();
        }

    }

    private static class NamedThreadFactory implements ThreadFactory {
        private static final AtomicInteger POOL_SEQ = new AtomicInteger(1);

        private final AtomicInteger mThreadNum = new AtomicInteger(1);

        private final String mPrefix;

        private final boolean mDaemon;

        private final ThreadGroup mGroup;

        public NamedThreadFactory() {
            this("XxlJobSchedule-" + POOL_SEQ.getAndIncrement(), true);
        }

        public NamedThreadFactory(String prefix, boolean daemon) {
            mPrefix = prefix + "-thread-";
            mDaemon = daemon;
            SecurityManager s = System.getSecurityManager();
            mGroup = (s == null) ? Thread.currentThread().getThreadGroup() : s.getThreadGroup();
        }

        @Override
        public Thread newThread(Runnable runnable) {
            String name = mPrefix + mThreadNum.getAndIncrement();
            Thread ret = new Thread(mGroup, runnable, name, 0);
            ret.setDaemon(mDaemon);
            return ret;
        }

    }

    private static final Logger LOGGER = LoggerFactory.getLogger(JobCenterAutoConfiguration.class);

    static final CloseableHttpClient httpClient = HttpClients.custom().disableAutomaticRetries().build();

    @Autowired
    private JobCenterProperties jobCenterProperties;

    @Autowired
    private Environment environment;

    @Autowired
    private ApplicationContext applicationContext;

    @Autowired(required = false)
    private DiscoveryClient discoveryClient;

    private String appName;

    private String discoveryAdminAddress() {
        String serviceId = jobCenterProperties.getServiceId();
        List<String> adminAddresList = new ArrayList<>();
        List<ServiceInstance> instanceList = discoveryClient.getInstances(serviceId);
        for (ServiceInstance serviceInstance : instanceList) {
            try {
                String host = serviceInstance.getHost();
                int port = serviceInstance.getPort();
                LOGGER.info("Connecting {} ({}:{})", serviceId, host, port);
                String httpAddress = "http://" + host + ":" + port + "/";
                if (isAdminReachable(httpAddress)) {
                    adminAddresList.add(httpAddress);
                } else {
                    LOGGER.error("Skip unreachable node {}", httpAddress);
                }
            } catch (Throwable e) {
                LOGGER.error("can not found node for admin!", e);
            }
        }
        if (adminAddresList.size() > 0) {
            adminAddresList.sort(Comparator.naturalOrder());
            return String.join(",", adminAddresList);
        } else {
            LOGGER.error("Jobcenter server is down,will try after 30s");
            return null;
        }
    }

    @SuppressWarnings("deprecation")
    private String discoveryLocalIp() {
        if (!StringUtils.isEmpty(jobCenterProperties.getLocalIp())) {
            LOGGER.error("should not run in PROD");
            InetAddress result = null;
            try {
                int lowest = Integer.MAX_VALUE;
                for (Enumeration<NetworkInterface> nics = NetworkInterface.getNetworkInterfaces();
                    nics.hasMoreElements();) {
                    NetworkInterface ifc = nics.nextElement();
                    if (ifc.isUp()) {
                        LOGGER.error("Testing interface: " + ifc.getDisplayName());
                        if (ifc.getIndex() < lowest || result == null) {
                            lowest = ifc.getIndex();
                        } else if (result != null) {
                            continue;
                        }
                        for (Enumeration<InetAddress> addrs = ifc.getInetAddresses(); addrs.hasMoreElements();) {
                            InetAddress address = addrs.nextElement();
                            if (address instanceof Inet4Address && !address.isLoopbackAddress()
                                && address.getHostAddress().equals(jobCenterProperties.getLocalIp())) {
                                LOGGER.error("Use localIp " + address.getHostAddress());
                                return address.getHostAddress();
                            }
                        }
                    }
                }
            } catch (IOException ex) {
                LOGGER.error("Cannot get first non-loopback address", ex);
            }
        }
        return InetUtils.getFirstNonLoopbackHostInfo().getIpAddress();
    }

    private String getCallBackLogPath() {
        ApplicationHome home = new ApplicationHome(JobCenterAutoConfiguration.class);
        File path = home.getDir();
        String logFilePath = path.getPath() + "/logs/xxljob";
        File logFile = new File(logFilePath);
        this.mkdir(logFile);
        return logFilePath;
    }

    @PostConstruct
    public void init() {
        this.initDefaultParm();
        ScheduledExecutorService scheduleReport = Executors.newScheduledThreadPool(1, new NamedThreadFactory());
        scheduleReport.scheduleAtFixedRate(new Runnable() {

            @Override
            public void run() {
                try {
                    XxlJobExecutor xxlJobExecutor = applicationContext.getBean(XxlJobExecutor.class);
                    String adminAddresses = discoveryAdminAddress();
                    if (StringUtils.isEmpty(adminAddresses)) {
                        return;
                    }
                    if (adminAddresses.equals(xxlJobExecutor.getAdminAddresses())) {
                        LOGGER.info("AdminAddresses has no changes.");
                        return;
                    }
                    if (xxlJobExecutor.getAdminAddresses() == null) {
                        LOGGER.info("Old adminAddresses is NULL");
                    } else {
                        LOGGER.info("Remove old node {}", xxlJobExecutor.getAdminAddresses());
                        XxlJobExecutor.getAdminBizList().clear();
                    }

                    LOGGER.info("Find all adminAddresses:" + adminAddresses + " from eureka");
                    xxlJobExecutor.setAdminAddresses(adminAddresses);
                    xxlJobExecutor.reInitAdminBizList();
                } catch (Throwable e) {
                    LOGGER.warn(e.getMessage(), e);
                }
            }
        }, 0, 30, TimeUnit.SECONDS);
    }

    public void initDefaultParm() {
        // appName
        String propertiesAppName = jobCenterProperties.getAppName();
        if (StringUtils.isBlank(propertiesAppName)) {
            propertiesAppName = environment.getProperty("spring.application.name");
        }
        if (StringUtils.isBlank(propertiesAppName)) {
            throw new RuntimeException("appName is null, You must config xxljob excutor name !!");
        } else {
            this.appName = propertiesAppName;
        }
    }

    private boolean isAdminReachable(String httpAddress) {
        HttpGet httpGet = new HttpGet(httpAddress);
        try {
            CloseableHttpResponse response = httpClient.execute(httpGet);
            EntityUtils.consume(response.getEntity());
            return response.getStatusLine().getStatusCode() == 200;
        } catch (Throwable e) {
            LOGGER.error(e.getMessage(), e);
            return false;
        } finally {
            httpGet.releaseConnection();
        }
    }

    private void mkdir(File file) {
        if (file.getParentFile().exists()) {
            file.mkdir();
        } else {
            mkdir(file.getParentFile());
            file.mkdir();
        }
    }

    @Bean(initMethod = "start", destroyMethod = "destroy")
    public XxlJobExecutor xxlJobExecutor() {
        LOGGER.info(">>>>>>>>>>> Jobcenter config init.");
        String adminAddresses = this.discoveryAdminAddress();
        String localIp = this.discoveryLocalIp();
        int port = jobCenterProperties.getPort();
        String accessToken = jobCenterProperties.getAccessToken();
        String callBackPath =
            jobCenterProperties.getLogPath() != null ? jobCenterProperties.getLogPath() : this.getCallBackLogPath();
        XxlJobExecutor xxlJobExecutor = new XxlJobExecutor();
        xxlJobExecutor.setAdminAddresses(adminAddresses);
        xxlJobExecutor.setAppName(appName);
        xxlJobExecutor.setIp(localIp);
        xxlJobExecutor.setPort(port);
        xxlJobExecutor.setAccessToken(accessToken);
        xxlJobExecutor.setLogPath(callBackPath);
        xxlJobExecutor.setLogRetentionDays(-1);
        xxlJobExecutor.setApplicationContext(applicationContext);
        return xxlJobExecutor;
    }

}