package cqt.goai.run.main;

import ch.qos.logback.classic.Level;
import ch.qos.logback.classic.LoggerContext;
import ch.qos.logback.classic.encoder.PatternLayoutEncoder;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.rolling.RollingFileAppender;
import ch.qos.logback.core.rolling.TimeBasedRollingPolicy;
import ch.qos.logback.core.util.OptionHelper;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import cqt.goai.exchange.ExchangeException;
import cqt.goai.exchange.ExchangeUtil;
import cqt.goai.model.other.RunInfo;
import cqt.goai.model.other.RunState;
import cqt.goai.run.Application;
import cqt.goai.run.annotation.Scheduled;
import cqt.goai.run.annotation.ScheduledScope;
import cqt.goai.run.notice.BaseNotice;
import cqt.goai.run.notice.EmailNotice;
import cqt.goai.run.notice.TelegramNotice;
import dive.common.crypto.AESUtil;
import dive.common.crypto.DHUtil;
import dive.http.common.MimeRequest;
import dive.http.common.model.Parameter;
import lombok.extern.slf4j.Slf4j;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.yaml.snakeyaml.Yaml;

import java.io.File;
import java.io.FileInputStream;
import java.lang.management.ManagementFactory;
import java.lang.management.RuntimeMXBean;
import java.lang.reflect.Method;
import java.util.*;
import java.util.concurrent.*;
import java.util.stream.Collectors;

import static cqt.goai.run.main.Const.DEFAULT_SCHEDULED_METHOD;
import static cqt.goai.run.main.Const.LEFT_SQUARE_BRACKETS;
import static dive.common.util.Util.exist;
import static dive.common.util.Util.useful;

/**
 * 管理类
 *
 * @author GOAi

 */
@Slf4j
public class Minister {

    /**
     * 单例
     * Singleton Pattern 单例模式
     * 只需要一个对象管理所有的配置和运行实例即可
     */
    private static Minister instance;

    /**
     * 所有程序运行相关信息
     */
    private final Secretary secretary = new Secretary();

    /**
     * 所有配置信息
     * @author GOAi
     */
    private class Secretary {
        /**
         * 标识系统状态，初始正在启动
         */
        RunInfo runInfo = new RunInfo(System.currentTimeMillis(), RunState.STARTING);

        /**
         * 策略名称
         */
        String strategyName = "";

        /**
         * 运行类名
         */
        String className = null;

        /**
         * 是否以debug模式启动
         */
        Boolean debug = false;

        /**
         * 多个实例配置
         */
        Map<Integer, JSONObject> configs = new HashMap<>();


        /**
         * 运行类
         */
        Class<?> taskClass = null;

        /**
         * 运行类中需要定时任务的方法
         */
        List<MethodInfo> methods = new LinkedList<>();

        /**
         * 多个运行实例
         */
        ConcurrentHashMap<Integer, TaskManager> managers = new ConcurrentHashMap<>();

        /**
         * 是否初始化完毕
         */
        boolean ready = false;

        private List<BaseNotice> notices;
    }

    private Minister() {} // 私有构造器

    /**
     * 获取单例
     */
    static Minister getInstance() {
        if (null == instance) {
            synchronized (Minister.class) {
                if (null == instance) {
                    instance = new Minister();
                }
            }
        }
        return instance;
    }

    boolean isDebug() {
        return this.secretary.debug;
    }

    /**
     * 运行入口
     * @param args 启动参数
     */
    public static void run(String[] args) {
        Minister.pid();
        Minister minister = getInstance();
        // 处理配置
        minister.config(args);
        try {
            minister.run(); // 运行
        } catch (Exception e) {
            e.printStackTrace();
            log.error(e.getMessage());

            minister.secretary.runInfo.setRunState(RunState.ERROR);
            minister.secretary.runInfo.setEndTime(System.currentTimeMillis());
            minister.updateRunInfo(minister.secretary.runInfo);

            System.exit(-1);
        }
    }

    /**
     * 获取配置信息 2种获取配置的方式
     *  1. args中的url参数
     *      例: --url=http://localhost:7758/config?token=token
     *      若配置--url表明从node请求获取配置信息
     *  2. 指定配置文件
     *      例: --name=/data/my_config.yml --> 全路径
     *          --name=my_config.yml --> 相对路径 运行jar同目录下的my_config.yml文件
     *                                  若jar同目录下无该文件，则从resources获取
     *      默认文件名为config.yml
     * @param args 启动参数
     */
    private void config(String[] args) {
        this.updateRunInfo(this.secretary.runInfo);

        // 尝试从网络获取配置
        JSONObject config = this.getConfigByUrl(args);

        if (!exist(config)) {
            // 从本地获取配置
            config = this.getConfigByLocal(args);
        }

        if (null != config) {
            this.config(config);
        } else {
            String message = "config can not be null";
            log.error(message);
            throw new ExchangeException(message);
        }
    }

    /**
     * 尝试从网络获取配置
     */
    private JSONObject getConfigByUrl(String[] args) {
        // 若存在--url参数，从node处获取config
        for (String arg : args) {
            if (arg.startsWith(Const.START_URL)) {
                String url = arg.substring(6);
                log.info("config url --> {}", url);

                String[] keys = DHUtil.dhKeyToBase64();
                String pk = keys[0];
                log.info("response config pk --> {}", pk);
                String response = MimeRequest.builder()
                        .url(url)
                        .post()
                        .body(Parameter.build("pk", pk).json(JSON::toJSONString))
                        .execute(ExchangeUtil.OKHTTP);

                if (!exist(response)) {
                    log.info("response data empty");
                    return new JSONObject();
                }
                JSONObject json = JSONObject.parseObject(response);
                if(json.getInteger("code") != 200){
                    log.info("response data error ---> {}", response);
                    return new JSONObject();
                }

                String pubKey = Util.getParamByUrl(url, "key");
                pubKey = new String(Base64.getDecoder().decode(pubKey));
                String configs = AESUtil.aesDecryptByBase64(json.getString("data"),
                        DHUtil.aesKeyToBase64(pubKey, keys[1]));
                if (json.containsKey("run_mode") && "debug".equals(json.getString("run_mode"))) {
                    log.info("configs -> {}", configs);
                }
                if (new File("debug").exists()) {
                    log.info("configs -> {}", configs);
                }
                return JSON.parseObject(configs);
            }
        }
        return null;
    }

    /**
     * 从本地文件获取配置
     */
    private JSONObject getConfigByLocal(String[] args) {
        // 默认配置文件名
        String name = "config.yml";
        // 若存在--name参数，更新配置文件名
        for (String arg : args) {
            if (arg.startsWith("--name=")) {
                name = arg.substring(7);
                log.info("config name change to: {}", name);
                break;
            }
        }

        log.info("load config from local");

        // 查找同目录下config.yml文件
        File file = new File(name);
        if (file.isAbsolute()) {
            // 若是绝对路径直接用
            log.info("load file by --name: {}", name);
        } else {
            // 获取运行路径
            String path = Application.class.getProtectionDomain()
                    .getCodeSource().getLocation().getPath();
            final String split = "/";
            if (!path.endsWith(split)) {
                path = path.substring(0, path.lastIndexOf(split)) + split;
            }
            if (new File(path + name).exists()) {
                log.info("load file {} : {}", name, path + name);
                // 全路径
                name = path + name;
                file = new File(name);
            } else {
                // 调用resource下config文件
                log.info("load file {} from resources", name);
            }
        }
        Map map;
        try {
            if (file.isAbsolute()) {
                map = new Yaml().load(new FileInputStream(file));
            } else {
                map = new Yaml().load(
                        Application.class.getClassLoader().getResourceAsStream(name));
            }
        } catch (Exception e) {
            e.printStackTrace();
            log.error(e.getMessage());
            String message = String.format("load file %s failed.", name);
            throw new RuntimeException(message);
        }
        return JSON.parseObject(JSON.toJSONString(map));
    }

    /**
     * 处理配置文件
     * @param config 配置文件
     */
    private void config(JSONObject config) {
        // 统一处理配置文件

        // 获取简单的配置
        this.configSimple(config);

        this.configNotices(config);

        // 获取每个实例的运行配置
        this.configs(config);
    }

    /**
     * 获取并设置简单的配置
     */
    private void configSimple(JSONObject config) {
        String strategyName = config.getString(Const.CONFIG_STRATEGY_NAME);
        if (useful(strategyName)) {
            this.secretary.strategyName = strategyName;
            log.info("strategy name --> {}", strategyName);
        } else {
            throw new ExchangeException("config '" + Const.CONFIG_STRATEGY_NAME + "' can not be null");
        }

        String className = config.getString(Const.CONFIG_CLASS_NAME);
        if (useful(className)) {
            this.secretary.className = className;
            log.info("task class name --> {}", className);
        } else {
            throw new ExchangeException("config '" + Const.CONFIG_CLASS_NAME + "' can not be null");
        }

        if (config.containsKey(Const.CONFIG_RUN_MODE) &&
                "debug".equalsIgnoreCase(config.getString(Const.CONFIG_RUN_MODE))) {
            this.secretary.debug = true;
            log.info("config run mode --> {}", config.getString(Const.CONFIG_RUN_MODE));
        }

    }

    /**
     * 获取通知配置
     * @param config 配置
     */
    private void configNotices(JSONObject config) {
        this.secretary.notices = new LinkedList<>();
        if (config.containsKey("notices")) {
            JSONArray ns = config.getJSONArray("notices");
            for (int i = 0; i < ns.size(); i++) {
                JSONObject c = ns.getJSONObject(i);
                switch (c.getString("type")) {
                    case "email" : this.secretary.notices.add(new EmailNotice(log, this.secretary.strategyName, c)); break;
                    case "telegram" : this.secretary.notices.add(new TelegramNotice(log, this.secretary.strategyName, c)); break;
                    default:
                }
            }
        }
    }

    /**
     * 获取并配置每个配置实例
     */
    private void configs(JSONObject config) {
        String tempConfigs = config.getString(Const.CONFIG_CONFIGS);
        if (tempConfigs.startsWith(LEFT_SQUARE_BRACKETS)) {
            JSONArray configs = config.getJSONArray(Const.CONFIG_CONFIGS);
            if (null == configs) {
                throw new ExchangeException("configs in config can not be null");
            }
            for (int i = 0; i < configs.size(); i++) {
                JSONObject c = configs.getJSONObject(i);
                Integer id = c.getInteger("id");
                if (null == id) {
                    log.error("can not mapping config, there is no id: {}", c);
                    continue;
                }
                if (id < 1) {
                    log.error("can not mapping config, id can not less then 1", c);
                    continue;
                }
                if (0 == i) {
                    this.getNotices(c);
                }
                this.secretary.configs.put(id, c);
            }
        } else {
            Integer id = 1;
            JSONObject c = config.getJSONObject(Const.CONFIG_CONFIGS);
            this.getNotices(c);
            this.secretary.configs.put(id, c);
        }
        log.info("config init configs size --> {}", this.secretary.configs.size());
    }

    /**
     * 兼容以前的配置
     * @param config 配置
     */
    private void getNotices(JSONObject config) {
        if (config.containsKey("telegramGroup")) {
            JSONObject c = new JSONObject();
            c.put("token", config.getString("telegramToken"));
            c.put("chatId", config.getString("telegramGroup"));
            this.secretary.notices.add(new TelegramNotice(log, this.secretary.strategyName,c));
        }
    }

    /**
     * 运行程序
     */
    private void run() {
        // 获取任务类信息
        Class<?> taskClass;
        try {
            taskClass = Class.forName(this.secretary.className);
        } catch (Exception e) {
            throw new ExchangeException("can not find(load) class: " + this.secretary.className);
        }

        // 测试实例化对象和是否为RunTask子类
        try {
            Object test = taskClass.newInstance();
            if (!(test instanceof RunTask)) {
                String message = "task class must extends RunTask: " + this.secretary.className;
                throw new ExchangeException(message);
            }
        } catch (InstantiationException | IllegalAccessException e) {
            throw new ExchangeException(
                    "class must has public non-parameter constructor: " + this.secretary.className);
        }

        this.secretary.taskClass = taskClass;

        // 检查定时任务
        this.checkSchedules(taskClass);
        this.checkSchedules(RunTask.class);
        this.checkLoop();

        // 启动任务
        this.secretary.configs.forEach(this::checkTask);

        log.info("start successful. instance size: {}", this.secretary.managers.size());

        this.updateRunInfo(this.secretary.runInfo.setRunState(RunState.STARTED));

        this.secretary.ready = true;

        ScheduledExecutorService running = new ScheduledThreadPoolExecutor(1,
                new ThreadPoolExecutor.DiscardPolicy());
        //这个定时任务，让程序不停止，否则，没有运行实例定时任务，程序就会结束
        running.scheduleAtFixedRate(this::checkManagers, 0, 1, TimeUnit.MINUTES);

        // 关闭程序回调
        Runtime.getRuntime().addShutdownHook(new Thread(
                () -> {
                    // 修改状态 -> 停止中
                    this.updateRunInfo(this.secretary.runInfo.setRunState(RunState.STOPPING));
                    // 调用正在运行任务的停止方法
                    this.secretary.managers.values()
                            .parallelStream().forEach(TaskManager::destroy);
                    // 修改状态 -> 已停止
                    this.updateRunInfo(this.secretary.runInfo.setRunState(RunState.STOPPED));
                    running.shutdown();
                }));
    }

    /**
     * 把定时任务检索出来
     */
    private void checkSchedules(Class<?> clazz) {
        Method[] methods = clazz.getDeclaredMethods();
        for (Method m : methods) {
            if (m.getName().equals(DEFAULT_SCHEDULED_METHOD)) {
                continue;
            }
            Scheduled[] schedules = m.getAnnotationsByType(Scheduled.class);
            if (exist(schedules) && 0 < schedules.length) {
                m.setAccessible(true);
                List<ScheduledInfo> sis = new ArrayList<>(schedules.length);
                for (Scheduled s : schedules) {
                    sis.add(new ScheduledInfo(s.cron(), s.fixedRate(), s.delay()));
                }
                if (!sis.isEmpty()) {
                    ScheduledScope scheduledScope = m.getAnnotation(ScheduledScope.class);
                    this.secretary.methods.add(new MethodInfo(m, null == scheduledScope ?
                            MethodScope.INSTANCE : scheduledScope.value(), sis));
                }
            }
        }
    }

    /**
     * 检查默认循环任务
     */
    private void checkLoop() {
        if (this.secretary.configs.isEmpty()) {
            return;
        }
        try {
            Method method = this.secretary.taskClass
                    .getDeclaredMethod(DEFAULT_SCHEDULED_METHOD);
            ScheduledScope scheduledScope = method.getAnnotation(ScheduledScope.class);
            method.setAccessible(true);
            this.secretary.methods.add(new MethodInfo(method, null == scheduledScope ?
                    MethodScope.INSTANCE : scheduledScope.value(),
                    Collections.singletonList(new ScheduledInfo(null, 1000, 1000))));
        } catch (NoSuchMethodException ignored) {
        }
    }

    /**
     * 启动一个实例
     * @param id 配置id
     * @param config 配置
     */
    private void checkTask(Integer id, JSONObject config) {
        ConcurrentHashMap<Integer, TaskManager> managers = this.secretary.managers;
        if (id < 0) {
            id = -id;
            // 停止指定任务
            if (managers.containsKey(id)) {
                managers.get(id).destroy();
                managers.remove(id);
            }
            return;
        }
        TaskManager manager = managers.get(id);
        if (exist(manager)) {
            // 如果已经有这个实例了，就关闭
            manager.destroy();
        }
        try {
            RunTask runTask = (RunTask) this.secretary.taskClass.newInstance();
            // 统一的配置从这里设置
            Logger log = this.initLog(id);
            runTask.init(this.secretary.strategyName, id, config, log, this.secretary.notices);
            manager = new TaskManager(id, runTask, log,
                    this.secretary.methods.stream().map(MethodInfo::copy).collect(Collectors.toList()),
                    this.secretary.debug);
        } catch (Exception e) {
            e.printStackTrace();
            log.error(e.getMessage());
        }
        if (null != manager) {
            managers.put(id, manager);
        } else {
            managers.remove(id);
        }
    }

    /**
     * 初始化日志
     */
    private Logger initLog(Integer id) {
        // 实例化log，每个实例存放地方不一致
        ch.qos.logback.classic.Logger logger = ((ch.qos.logback.classic.Logger)
                LoggerFactory.getLogger(this.secretary.strategyName + "-" + id));

        LoggerContext context = logger.getLoggerContext();

        TimeBasedRollingPolicy<ILoggingEvent> policy = new TimeBasedRollingPolicy<>();
        policy.setFileNamePattern(OptionHelper.substVars(
                "logs/past/" + id + "/%d{yyyy-MM-dd}.log.gz", context));
        policy.setMaxHistory(31);
        policy.setContext(context);

        PatternLayoutEncoder encoder = new PatternLayoutEncoder();
        encoder.setContext(context);
        encoder.setPattern("%d{yyyy-MM-dd HH:mm:ss.SSS} %5level - [%thread] %logger : %msg%n");

        RollingFileAppender<ILoggingEvent> appender = new RollingFileAppender<>();
        appender.setContext(context);
        appender.setName(this.secretary.strategyName + "-" + id);
        appender.setFile(OptionHelper.substVars("logs/" + id + "/log.log", context));
        appender.setAppend(true);
        // 同一文件多输入完整检查
        appender.setPrudent(false);
        appender.setRollingPolicy(policy);
        appender.setEncoder(encoder);
        policy.setParent(appender);

        policy.start();
        encoder.start();
        appender.start();

        logger.setLevel(Level.INFO);
        // 终端输出
        logger.setAdditive(true);
        logger.addAppender(appender);

        return logger;
    }

    /**
     * 核对检查运行状态，未运行的要运行，停止的要关闭
     */
    private void checkManagers() {
        Set<Integer> need = this.secretary.configs.keySet();
        Set<Integer> running = this.secretary.configs.keySet();
        Set<Integer> start = new HashSet<>();
        Set<Integer> stop = new HashSet<>();
        for (Integer id : need) {
            if (!running.contains(id)) {
                start.add(id);
            }
        }
        for (Integer id : running) {
            if (!need.contains(id)) {
                stop.add(id);
            }
        }
        start.forEach(id -> this.checkTask(id, this.secretary.configs.get(id)));
        stop.forEach(id -> this.checkTask(-id, this.secretary.configs.get(id)));
    }

    /**
     * 获取TaskManager
     */
    Map<Integer, TaskManager> getManagers() {
        return this.secretary.managers;
    }

    boolean isReady() {
        return this.secretary.ready;
    }

    /**
     * 更新系统状态
     */
    private void updateRunInfo(RunInfo info) {
        if (info.getRunState() == RunState.STOPPED) {
            info.setEndTime(System.currentTimeMillis());
        }
        File file = new File(".run_info");
        if (Util.checkFile(file)) {
            Util.writeFile(file, JSON.toJSONString(info));
        }
    }

    /**
     * 是否正在停止
     * @return 是否正在停止
     */
    boolean isStopping() {
        return this.secretary.runInfo.getRunState() == RunState.STOPPING;
    }

    // ==================== tools ====================

    /**
     * 输出程序启动的pid
     */
    private static void pid() {
        RuntimeMXBean runtime = ManagementFactory.getRuntimeMXBean();
        String name = runtime.getName();
        log.info("process mark: {}", name);
        int index = name.indexOf("@");
        if (index != -1) {
            int pid = Integer.parseInt(name.substring(0, index));
            getInstance().secretary.runInfo.setPid(pid);
            log.info("process id: {}", pid);
            File file = new File("PID");
            if (Util.checkFile(file)) {
                Util.writeFile(file, String.valueOf(pid));
            }
        }
    }

}
