/*
 * Copyright (C) 2016-2022 ActionTech.
 * License: http://www.gnu.org/licenses/gpl.html GPL version 2 or higher.
 */

package com.actiontech.dble.singleton;

import com.actiontech.dble.DbleServer;
import com.actiontech.dble.config.model.SystemConfig;
import com.actiontech.dble.util.IOUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.File;
import java.io.IOException;
import java.lang.reflect.Field;
import java.nio.charset.Charset;
import java.util.concurrent.TimeUnit;

// only work for linux
public final class CustomMySQLHa {
    private static final Logger LOGGER = LoggerFactory.getLogger(CustomMySQLHa.class);
    private static final CustomMySQLHa INSTANCE = new CustomMySQLHa();
    /**
     * notice:
     * 1. this {@code process} variable is null before first init. once this set to notNull. It won't set to be null again.
     * 2. this {@code process} variable isn't thread safe.
     */
    private volatile Process process;

    private CustomMySQLHa() {
    }

    public static CustomMySQLHa getInstance() {
        return INSTANCE;
    }

    // return null if success
    public synchronized String start() {
        if (SystemConfig.getInstance().isUseOuterHa()) {
            String msg = "You use OuterHa or Cluster, please use the third party HA Component";
            LOGGER.debug(msg);
            return msg;
        }
        if (isProcessAlive()) {
            return "python process exists";
        }
        String exe = "python3";
        String file = SystemConfig.getInstance().getHomePath() + File.separatorChar + "bin" + File.separatorChar + "custom_mysql_ha.py";
        String[] cmdArr = new String[]{exe, file};
        if (LOGGER.isDebugEnabled()) {
            LOGGER.debug("a new  process `" + exe + " " + file + "` will be execute ");
        }
        try {
            process = Runtime.getRuntime().exec(cmdArr);
            synchronized (process) {
                /*
                wait for a while to catch most of bootstrap errors.
                 */
                final boolean nonTimeout = process.waitFor(200, TimeUnit.MILLISECONDS);
                if (nonTimeout) {
                    String msg;
                    final int exitValue = process.exitValue();
                    if (exitValue != 0) {
                        msg = "starting simple_ha_switch error with exitCode:" + exitValue;
                        LOGGER.warn("{},script error log :`{}`", msg, IOUtil.convertStreamToString(process.getErrorStream(), Charset.defaultCharset()));
                        msg += ", view logs for more details";
                    } else {
                        msg = "starting simple_ha_switch exit with exitCode:" + exitValue;
                        LOGGER.warn(msg);
                    }
                    return msg;
                }
            }
            DbleServer.getInstance().getComplexQueryExecutor().execute(() -> {
                try {
                    synchronized (process) {
                        /*
                        waitFor() will call process.wait()
                        wait() will release the synchronized lock of 'process'.
                         */
                        final int exitValue = process.waitFor();

                        if (exitValue != 0) {
                            LOGGER.warn("starting simple_ha_switch error with exitCode:{},script error log is `{}`", exitValue, IOUtil.convertStreamToString(process.getErrorStream(), Charset.defaultCharset()));
                        }
                    }

                } catch (InterruptedException | IOException e) {
                    String msg = "starting simple_ha_switch occurred IOException";
                    LOGGER.warn(msg, e);
                }

            });

        } catch (IOException | InterruptedException e) {
            String msg = "starting simple_ha_switch occurred " + e.getClass().getSimpleName();
            LOGGER.warn(msg, e);
            msg += ", view logs for more details";
            return msg;
        }
        return null;
    }

    // return null if success
    public synchronized String stop(boolean byHook) {
        if (SystemConfig.getInstance().isUseOuterHa()) {
            String msg = "You use OuterHa or Cluster, please use the third party HA Component";
            if (byHook) {
                System.out.println("You use OuterHa or Cluster, no need to clean up ha process");
            } else if (LOGGER.isDebugEnabled()) {
                LOGGER.debug(msg);
            }
            return msg;
        }
        if (!isProcessAlive()) {
            String msg = "python process does not exists";
            if (byHook) {
                System.out.println(msg);
            } else {
                LOGGER.warn(msg);
            }
            return msg;
        }
        long pid = getPidOfLinux(byHook);
        if (pid == -1) {
            return "get Pid Of Python Script failed";
        }
        String exe = "kill";
        String command = "-9";
        String[] cmdArr = new String[]{exe, command, Long.toString(pid)};
        try {
            Process killProcess = Runtime.getRuntime().exec(cmdArr);
            int returnCode = killProcess.waitFor();
            String msg = "the result of `kill -9 " + pid + "` is " + returnCode;
            if (byHook) {
                System.out.println(msg);
            } else {
                LOGGER.debug(msg);
            }
            synchronized (process) {
                //Maybe this destroy operation is redundant. Not 100% sure.
                process.destroyForcibly();
                process.waitFor();
            }
            if (returnCode != 0) {
                return msg;
            } else {
                return null;
            }
        } catch (IOException e) {
            String msg = "killing simple_ha_switch occurred IOException";
            if (byHook) {
                System.out.println(msg + e);
            } else {
                LOGGER.warn(msg, e);
            }
            return msg;
        } catch (InterruptedException e) {
            String msg = "killing simple_ha_switch was Interrupted";
            if (byHook) {
                System.out.println(msg + e);
            } else {
                LOGGER.warn(msg, e);
            }
            return msg;
        }
    }

    public boolean isProcessAlive() {
        if (process != null) {
            synchronized (process) {
                return process.isAlive();
            }
        }
        return false;
    }

    private long getPidOfLinux(boolean byHook) {
        long pid = -1;

        try {
            if (process != null) {
                synchronized (process) {
                    if (process.getClass().getName().equals("java.lang.UNIXProcess")) {
                        Field f = process.getClass().getDeclaredField("pid");
                        f.setAccessible(true);
                        pid = f.getLong(process);
                        f.setAccessible(false);
                    }
                }
            }

        } catch (Exception e) {
            if (byHook) {
                System.out.println("getPidOfLinux failed:" + e);
            } else {
                LOGGER.warn("getPidOfLinux failed:", e);
            }
            pid = -1;
        }
        if (byHook) {
            System.out.println("python pid is " + pid);
        } else if (LOGGER.isDebugEnabled()) {
            LOGGER.debug("python pid is " + pid);
        }

        return pid;
    }
}
