package com.jetbrains.cef.remote;

import com.jetbrains.cef.remote.thrift.transport.TSocket;
import com.jetbrains.cef.remote.thrift.transport.TTransportException;
import org.cef.CefSettings;
import org.cef.OS;
import org.cef.misc.CefLog;
import org.cef.misc.Utils;

import java.io.*;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.StandardProtocolFamily;
import java.net.UnixDomainSocketAddress;
import java.nio.channels.SocketChannel;
import java.nio.file.InvalidPathException;
import java.nio.file.Path;
import java.util.*;
import java.util.function.BooleanSupplier;

public class NativeServerManager {
    public static final String ALT_CEF_SERVER_PATH = Utils.getString("ALT_CEF_SERVER_PATH");
    private static final int WAIT_LOOP_SLEEP_MS = Utils.getInteger("JCEF_WAIT_LOOP_SLEEP_MS", 200);

    public static boolean isProcessAlive(ThriftTransport thriftServer) {
        Process p = ServerStarter.ourNativeServerProcesses.get(thriftServer.toString());
        return p != null && p.isAlive();
    }

    public static boolean isConnectable(int port) {
        return isConnectable(new ThriftTransport(port), false);
    }

    public static boolean isConnectable(ThriftTransport thriftServer) {
        return isConnectable(thriftServer, false);
    }

    private static boolean isConnectable(ThriftTransport thriftServer, boolean withDebug) {
        try {
            if (thriftServer.isTcp()) {
                try {
                    TSocket socket = new TSocket("localhost", thriftServer.getPort());
                    socket.open();
                    socket.close();
                    if (withDebug)
                        CefLog.Debug("isConnectable: tcp-port %d, opened and connected.", thriftServer.getPort());
                    return true;
                } catch (TTransportException e) {
                    if (withDebug)
                        CefLog.Debug("isConnectable: tcp-port %d, TTransportException occurred: %s", thriftServer.getPort(), e.getMessage());
                }
                return false;
            }
            try {
                if (OS.isWindows()) {
                    WindowsPipeSocket pipe = new WindowsPipeSocket(thriftServer.getPipe());
                    pipe.close();
                    if (withDebug)
                        CefLog.Debug("isConnectable: win-pipe '%s', opened and connected.", thriftServer.getPipe());
                    return true;
                }
                UnixDomainSocketAddress socketAddress = UnixDomainSocketAddress.of(thriftServer.getPipe());
                SocketChannel channel = SocketChannel.open(StandardProtocolFamily.UNIX);
                channel.connect(socketAddress);
                channel.close();
                if (withDebug)
                    CefLog.Debug("isConnectable: pipe '%s', opened and connected.", thriftServer.getPipe());
                return true;
            } catch (IOException e) {
                if (withDebug)
                    CefLog.Debug("isConnectable: pipe '%s', IOException occurred: %s", thriftServer.getPipe(), e.getMessage());
            }
        } catch (Throwable e) {
            CefLog.Error("isConnectable: exception %s", e.getMessage());
        }
        return false;
    }

    private static boolean isServerSocketBusy(int port, boolean withDebug) {
        try {
            ServerSocket serverSocket = null;
            try {
                serverSocket = new ServerSocket(port, 1, InetAddress.getByName("localhost"));
            } catch (Throwable e) {
                if (withDebug)
                    CefLog.Debug("isServerSocketBusy: can't open tcp-port %d, exception occurred: %s", port, e.getMessage());
                return true;
            }
            if (withDebug)
                CefLog.Debug("isServerSocketBusy: tcp-port %d, successfully opened.", port);
            serverSocket.close();
        } catch (Throwable e) {
            CefLog.Error("isServerSocketBusy: exception %s", e.getMessage());
        }
        return false;
    }

    public static String isRunning(ThriftTransport thriftServer) {
        return isRunning(thriftServer, false);
    }

    // returns root_cache_path of running server (or null if not running)
    public static String isRunning(ThriftTransport transport, boolean withDebug) {
        if (ServerStarter.ourNativeServerProcesses.get(transport.toString()) != null && !ServerStarter.ourNativeServerProcesses.get(transport.toString()).isAlive()) {
            if (withDebug)
                CefLog.Debug("isRunning: server process is not alive.");
            return null;
        }
        try {
            if (transport.isTcp()) {
                // At first, we check whether the server socket is busy.
                if (!isServerSocketBusy(transport.getPort(), withDebug))
                    return null;
                // Well, socket is busy and server seems to be running. Let's try to connect to it.
            }

            if (!isConnectable(transport, withDebug))
                return null;

            // Successfully connected to server transport => server seems to be running. Let's connect and check an echo.
            RpcExecutor test;
            try {
                test = new RpcExecutor().openTransport(transport);
            } catch (TTransportException e) {
                if (withDebug)
                    CefLog.Debug("isRunning: TTransportException occurred when open server transport: %s", e.getMessage());
                return null;
            }
            String testMsg = "test_message786";
            String echoMsg = test.execObj(s -> s.echo(testMsg));
            String root = null;
            final boolean isEchoCorrect = echoMsg != null && echoMsg.equals(testMsg);
            if (!isEchoCorrect)
                CefLog.Error("isRunning: cef_server seems to be running, but echo is incorrect: '%s' (original '%s')", echoMsg, testMsg);
            else {
                root = test.execObj(s -> s.getServerInfo("root"));
                if (withDebug)
                    CefLog.Debug("isRunning: cef_server is running and echo is correct, root='%s'", root);
            }
            test.closeTransport();
            return isEchoCorrect ? root : null;
        } catch (Throwable e) {
            CefLog.Error("isRunning: exception %s", e.getMessage());
        }
        return null;
    }

    public static String getServerState(ThriftTransport thriftServer) {
        try {
            RpcExecutor test = new RpcExecutor().openTransport(thriftServer);
            String state = test.execObj(s -> s.getServerInfo("state"));
            test.closeTransport();
            return state;
        } catch (TTransportException e) {
            return "stopped";
        }
    }

    // returns true when server was stopped successfully
    public static boolean stopAndWait(ThriftTransport thriftServer, long timeoutMs) {
        CefLog.Debug("Stop running cef_server instance.");
        try {
            RpcExecutor test = new RpcExecutor().openTransport(thriftServer);
            String state = test.execObj(s -> s.getServerInfo("state"));
            CefLog.Debug("Server state before stop: %s", state);
            test.exec(s -> s.stop());
            test.closeTransport();
        } catch (TTransportException e) {
            CefLog.Debug("Exception when trying to stop server, err: %s", e.getMessage());
        }

        // Wait for stopping
        boolean stopped = waitForStopped(thriftServer, timeoutMs);
        if (!stopped) {
            CefLog.Error("Can't stop server in %d ms (process is %s)", timeoutMs, isProcessAlive(thriftServer) ? "alive" : "dead");
            CefLog.Debug("Server state: %s", getServerState(thriftServer));
            return false;
        }
        ServerStarter.ourNativeServerProcesses.remove(thriftServer.toString());
        return true;
    }

    private static boolean isDefaultRoot(String rootPath) {
        if (OS.isWindows())
            return rootPath.compareToIgnoreCase("~\\AppData\\Local\\CEF\\User Data") == 0;
        if (OS.isLinux())
            return rootPath.compareToIgnoreCase("~/.config/cef_user_data") == 0;
        return rootPath.compareToIgnoreCase("~/Library/Application Support/CEF/User Data") == 0;
    }

    public static boolean fixRootInSettings(CefSettings settings, String newRootDirName) {
        try {
            return fixRootInSettingsImpl(settings, newRootDirName);
        } catch (Throwable e) {
            CefLog.Error("Can't fix root_cache_path in settings: %s", e.getMessage());
        }
        return false;
    }

    private static boolean fixRootInSettingsImpl(CefSettings settings, String newRootDirName) {
        List<String> runningInstancesRoots = ProcessLister.findRunningInstancesRoots();
        if (runningInstancesRoots == null || runningInstancesRoots.isEmpty())
            return false;

        if (settings.cache_path != null && !settings.cache_path.isEmpty()) {
            Path settingsRoot;
            try {
                settingsRoot = Path.of(settings.cache_path);
            } catch (InvalidPathException e) {
                CefLog.Error("Can't find path '%s': %s", settings.cache_path, e.getMessage());
                return false;
            }
            for (String sr : runningInstancesRoots) {
                Path r;
                try {
                    r = Path.of(sr);
                } catch (InvalidPathException e) {
                    CefLog.Error("Can't find path '%s': %s", sr, e.getMessage());
                    continue;
                }
                if (r.equals(settingsRoot)) {
                    settings.cache_path = Path.of(System.getProperty("java.io.tmpdir")).resolve(newRootDirName).toString();
                    CefLog.Info("Non-empty settings.cache_path='%s' conflicts with existing root_cache_path, will be replaced with '%s'.", r, settings.cache_path);
                    return true;
                }
            }
        } else {
            // settings.cache_path == null
            for (String sr: runningInstancesRoots) {
                if (NativeServerManager.isDefaultRoot(sr)) {
                    settings.cache_path = Path.of(System.getProperty("java.io.tmpdir")).resolve(newRootDirName).toString();
                    CefLog.Info("Empty settings.cache_path will be replaced with '%s' (because found CEF instance with system-default root_cache_path '%s')", settings.cache_path, sr);
                    return true;
                }
            }
        }
        return false;
    }

    public static boolean waitForRunning(ThriftTransport thriftServer, long timeoutMs) {
        return waitFor(() -> isRunning(thriftServer) != null, timeoutMs, thriftServer.toStringShort() + " starting");
    }

    public static boolean waitForStopped(ThriftTransport thriftServer, long timeoutMs) {
        return waitFor(() -> isRunning(thriftServer) == null, timeoutMs, thriftServer.toStringShort() + " stopping");
    }

    private static boolean waitFor(BooleanSupplier checker, long timeoutMs, String hint) {
        final long startNs = System.nanoTime();
        boolean success;
        do {
            try {
                Thread.sleep(WAIT_LOOP_SLEEP_MS);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            CefLog.Debug("Waiting for server %s", hint);
            success = checker.getAsBoolean();
        } while (!success && (System.nanoTime() - startNs < timeoutMs*1000000));

        return success;
    }

    public static boolean isRemoteSupported() {
        File cef_server_exe = getServerExe();
        if (cef_server_exe == null) {
            // NOTE: cef_server can be manually started on custom port (for example, in debugging)
            return ThriftTransport.MANUAL_SERVER_SELECT || isRunning(ThriftTransport.ourDefaultServer) != null;
        }
        return cef_server_exe.exists() && !cef_server_exe.isDirectory();
    }

    static File getServerExe() {
        if (ALT_CEF_SERVER_PATH != null && !ALT_CEF_SERVER_PATH.trim().isEmpty())
            return new File(ALT_CEF_SERVER_PATH);

        ProcessHandle.Info i = ProcessHandle.current().info();
        String cmd = i.command().get();
        if (cmd == null || cmd.isEmpty()) {
            CefLog.Warn("Can't determine cef_server location via ProcessHandle (because the command is empty).");
            return findExeViaSystemProperty();
        }

        final boolean isJava = OS.isWindows() ? cmd.endsWith("java.exe") : cmd.endsWith("java");
        if (isJava) {
            File javabin = new File(cmd);
            if (!javabin.exists() || javabin.isDirectory()) {
                CefLog.Warn("Can't determine cef_server location via ProcessHandle (because calculated java.exe doesn't exist), cmd=%s");
                return findExeViaSystemProperty();
            }
            File result;
            if (OS.isMacintosh())
                result = new File(javabin.getParentFile().getParentFile().getParentFile(), "Frameworks/cef_server.app/Contents/MacOS/cef_server");
            else if (OS.isLinux())
                result = new File(javabin.getParentFile().getParentFile(), "lib/cef_server");
            else
                result = new File(javabin.getParentFile(), "cef_server.exe");
            if (!result.exists()) {
                CefLog.Warn("Can't determine cef_server location via java-process path '%s' (because calculated path '%s' doesn't exist), cmd=%s", javabin.getAbsolutePath(), result.getAbsolutePath(), cmd);
                return findExeViaSystemProperty();
            }

            return result;
        }

        //
        // It seems that JVM is started via the native launcher.
        //

        File result = findExeViaSystemProperty();
        if (result != null) {
            CefLog.Debug("Java is started via native launcher. Found cef_server path %s (via system propety)", result.getAbsolutePath());
            return result;
        }

        // TODO: get path of loaded libjvm and calculate relative server path
        File launcher = new File(cmd);
        if (!launcher.exists()) {
            CefLog.Warn("Can't find cef_server in bundled jbr (launcher '%s' doesn't exist), cmd=%s", launcher.getAbsolutePath(), cmd);
            return null;
        }

        if (OS.isMacintosh())
            result = new File(launcher.getParentFile().getParentFile(), "jbr/Contents/Frameworks/cef_server.app/Contents/MacOS/cef_server");
        else if (OS.isLinux())
            result = new File(launcher.getParentFile().getParentFile(), "jbr/lib/cef_server");
        else
            result = new File(new File(new File(launcher.getParentFile().getParentFile(), "jbr"), "bin"), "cef_server.exe");

        if (!result.exists()) {
            CefLog.Warn("Can't find cef_server in bundled jbr (calculated path '%s' doesn't exist), cmd=%s", result.getAbsolutePath(), cmd);
            return null;
        }
        CefLog.Debug("Java is started via native launcher. Found cef_server path %s (in bundled jbr)", result.getAbsolutePath());
        return result;

    }

    private static File findExeViaSystemProperty() {
        String javaPath = System.getProperty("java.home");
        if (javaPath == null || javaPath.isEmpty()) {
            CefLog.Error("Can't find cef_server binary: system property 'java.home' is empty.");
            return null;
        }

        File javaDir = new File(javaPath);
        if (!javaDir.exists() || !javaDir.isDirectory()) {
            CefLog.Error("Can't find cef_server binary via System.getProperty('java.home'): java directory doesn't exist, 'java.home'=%s", javaPath);
            return null;
        }

        File result;
        if (OS.isMacintosh()) // javaPath points to Home: /Applications/IntelliJ IDEA Ultimate 2024.3 Nightly.app/Contents/jbr/Contents/Home
            result = new File(javaDir.getParentFile(), "Frameworks/cef_server.app/Contents/MacOS/cef_server");
        else if (OS.isLinux())
            result = new File(javaDir, "lib/cef_server");
        else
            result = new File(new File(javaDir, "bin"), "cef_server.exe");

        if (!result.exists()) {
            CefLog.Debug("Can't find cef_server binary via System.getProperty('java.home'): file %s doesn't exist, 'java.home'=%s", result.getAbsolutePath(), javaPath);
            return null;
        }
        CefLog.Debug("Found cef_server binary '%s' via System.getProperty('java.home')=%s", result.getAbsolutePath(), javaPath);
        return result;
    }

    public static class ServerLogLevel {
        public final static int LEVEL_DISABLED = 100;
        public final static int LEVEL_FATAL = 10;
        public final static int LEVEL_ERROR = 9;
        public final static int LEVEL_WARN = 8;
        public final static int LEVEL_INFO = 7;
        public final static int LEVEL_DEBUG = 6;
        public final static int LEVEL_TRACE = 5;

        public static int cef2native(CefSettings.LogSeverity severity) {
            if (severity == CefSettings.LogSeverity.LOGSEVERITY_DISABLE)
                return LEVEL_DISABLED;
            else if (severity == CefSettings.LogSeverity.LOGSEVERITY_DEFAULT)
                return LEVEL_INFO;
            else if (severity == CefSettings.LogSeverity.LOGSEVERITY_FATAL)
                return LEVEL_FATAL;
            else if (severity == CefSettings.LogSeverity.LOGSEVERITY_ERROR)
                return LEVEL_ERROR;
            else if (severity == CefSettings.LogSeverity.LOGSEVERITY_WARNING)
                return LEVEL_WARN;
            else if (severity == CefSettings.LogSeverity.LOGSEVERITY_INFO)
                return LEVEL_DEBUG;
            else if (severity == CefSettings.LogSeverity.LOGSEVERITY_VERBOSE)
                return LEVEL_TRACE;
            return LEVEL_DISABLED;
        }

        public static String nativeDesc(int level) {
            if (level == LEVEL_DISABLED)
                return "disabled";
            if (level == LEVEL_FATAL)
                return "fatal";
            if (level == LEVEL_ERROR)
                return "error";
            if (level == LEVEL_WARN)
                return "warn";
            if (level == LEVEL_INFO)
                return "info";
            if (level == LEVEL_DEBUG)
                return "debug";
            if (level == LEVEL_TRACE)
                return "trace";
            return "unknown_logging_level_" + level;
        }

        static String cef2native_str(CefSettings.LogSeverity severity) {
            if (severity == CefSettings.LogSeverity.LOGSEVERITY_DISABLE)
                return "disable";
            else if (severity == CefSettings.LogSeverity.LOGSEVERITY_DEFAULT)
                return "info";
            else if (severity == CefSettings.LogSeverity.LOGSEVERITY_FATAL)
                return "fatal";
            else if (severity == CefSettings.LogSeverity.LOGSEVERITY_ERROR)
                return "err";
            else if (severity == CefSettings.LogSeverity.LOGSEVERITY_WARNING)
                return "warn";
            else if (severity == CefSettings.LogSeverity.LOGSEVERITY_INFO)
                return "debug";
            else if (severity == CefSettings.LogSeverity.LOGSEVERITY_VERBOSE)
                return "verb";
            return "disable";
        }

        public static int str2native(String level) {
            if (level == null || level.isEmpty())
                return LEVEL_DISABLED;

            level = level.toLowerCase();
            if (level.contains("disable"))
                return LEVEL_DISABLED;
            if (level.contains("fatal"))
                return LEVEL_FATAL;
            if (level.contains("err"))
                return LEVEL_ERROR;
            if (level.contains("warn"))
                return LEVEL_WARN;
            if (level.contains("info"))
                return LEVEL_INFO;
            if (level.contains("debug"))
                return LEVEL_DEBUG;
            if (level.contains("trace"))
                return LEVEL_TRACE;

            return LEVEL_INFO;
        }
    }
}
