/**
 * (c) Copyright HCL Technologies Ltd. 2018. All Rights Reserved.
 * This is licensed under the following license.
 * The Eclipse Public 1.0 License (http://www.eclipse.org/legal/epl-v10.html)
 */

package com.urbancode.air.plugin.AppScanSaaS

import java.util.concurrent.TimeUnit
import java.util.List
import java.util.Properties

/**
 * Helper class that provides all of the functions for running shell (Linux/Mac) or console (Windows)
 * commands to start and stop presences on the agent machine.
 */
class PresenceHelper {
    private final String serviceWorkingDirectory = "AppscanPresenceService"
    private final String agentServiceJar = "agentService.jar"
    private final String clientJar = "TunnelClient.jar"
    private final String windowsScript = "startPresence.vbs"
    private final String unixScript = "startPresence.sh"
    private final String windowsServiceScript = "StartPresenceAsService.bat"
    private final String windowsPresenceExe = "Presence.exe"
    private final String windowsPresenceUpdaterExe = "PresenceUpdater.exe"
    private final String linuxPresenceBinary = "Presence"

    private RestClient restClient
    private boolean isWindows

    public PresenceHelper(RestClient restClient, boolean isWindows) {
        this.restClient = restClient
        this.isWindows = isWindows
    }

    public String createPresence(boolean start) {
        println("[Action] Preparing Private Site Scanning Tunnel Server Setup.")

        String newPresenceId = restClient.createNewPresence()

        if (start) {
            startPresence(newPresenceId, true)  // Create presence and generate new key
        }

        return newPresenceId
    }

    public void stopPresence(String presenceId) {
        restClient.downloadAppscanPresence(serviceWorkingDirectory, isWindows, presenceId)
        stopRunningPresence()
    }

    private String startPresence(String presenceId, boolean renewKey) {
        println("[DEBUG] Starting Presence for ID: " + presenceId);
        println("[DEBUG] Working Directory: " + serviceWorkingDirectory);

        // Ensure no stale Presence process is still holding binaries before refresh.
        println("[DEBUG] Stopping any running Presence before refresh...");
        stopRunningPresence();

        restClient.downloadAppscanPresence(serviceWorkingDirectory, isWindows, presenceId);
        println("[DEBUG] Presence package downloaded.");
        configurePresenceSslValidation(new File(serviceWorkingDirectory));

        File dir = new File(serviceWorkingDirectory);

        File[] files = dir.listFiles();
        if (files != null) {
            for (File f : files) {
                if (f.getName().toLowerCase().contains("key")) {
                    println("[DEBUG] Deleting old key file: " + f.getAbsolutePath());
                    f.delete();
                }
            }
        }

        if (renewKey) {
            println("[DEBUG] Renewing Presence key...");
            restClient.renewPresenceKeyFile(serviceWorkingDirectory, presenceId);
            println("[DEBUG] Presence key renewed.");
        }

        File newKey = new File(serviceWorkingDirectory, "AppScanPresence.key");
        File expectedKey = new File(serviceWorkingDirectory, "Presence.key");

        if (newKey.exists()) {
            println("[DEBUG] Renaming key file to Presence.key");
            newKey.renameTo(expectedKey);
        }

        if (!expectedKey.exists() || expectedKey.length() == 0) {
            throw new RuntimeException("Valid Presence.key file not found!");
        }

        println("[DEBUG] Key file ready: " + expectedKey.getAbsolutePath());

        println("[DEBUG] Stopping any running Presence before launch...");
        stopRunningPresence();

        List<String> args = new ArrayList<>();
        boolean launchedDirectProcess = false;

        if (isWindows) {
            File windowsPresenceExecutable = new File(serviceWorkingDirectory, windowsPresenceExe);

            if (!windowsPresenceExecutable.exists()) {
                throw new RuntimeException("Presence.exe not found in " + serviceWorkingDirectory);
            }

            args.add(windowsPresenceExecutable.getAbsolutePath());
            launchedDirectProcess = true;

            println("[DEBUG] Using Presence.exe directly: " + windowsPresenceExecutable.getAbsolutePath());

        } else {
            File linuxPresenceExecutable = new File(serviceWorkingDirectory, linuxPresenceBinary);

            if (!linuxPresenceExecutable.exists()) {
                throw new RuntimeException("Presence binary not found in " + serviceWorkingDirectory);
            }

            linuxPresenceExecutable.setExecutable(true);
            args.add(linuxPresenceExecutable.getAbsolutePath());

            println("[DEBUG] Using Linux binary: " + linuxPresenceExecutable.getAbsolutePath());
        }

        println("[ACTION] Starting AppScan Presence with command: " + args);

        try {
            Process process = args.execute(null, new File(serviceWorkingDirectory));

            println("[DEBUG] Process started. Capturing output...");

            Thread.startDaemon {
                process.inputStream.eachLine { line ->
                    println("[PRESENCE-OUT] " + line);
                }
            }

            Thread.startDaemon {
                process.errorStream.eachLine { line ->
                    println("[PRESENCE-ERR] " + line);
                }
            }

            Thread.sleep(10000);

        } catch (Exception e) {
            println("[ERROR] Failed to start Presence: " + e.getMessage());
            e.printStackTrace();
            throw e;
        }

        println("[DEBUG] Verifying Presence ID with server...");
        restClient.verifyPresenceId(presenceId);

        println("[SUCCESS] Presence started successfully.");
        return presenceId;
    }

    private void configurePresenceSslValidation(File serviceDirectory) {
        File appSettingsFile = findAppSettingsFile(serviceDirectory)
        if (appSettingsFile == null) {
            println("[Warning] Presence appsettings.json not found under " + serviceDirectory.getAbsolutePath() + ". Skipping SSL bypass configuration.")
            return
        }

        String content = appSettingsFile.getText("UTF-8")
        boolean bypassSslValidation = !restClient.validateSSL
        String newline = content.contains("\r\n") ? "\r\n" : "\n"

        // Presence reads this setting from PresenceOptions, not from the root object.
        int[] presenceOptionsBounds = findNamedObjectBounds(content, "PresenceOptions")
        if (presenceOptionsBounds == null) {
            throw new RuntimeException("Could not find 'PresenceOptions' object in " + appSettingsFile.getAbsolutePath())
        }

        int objectStart = presenceOptionsBounds[0]
        int objectEnd = presenceOptionsBounds[1]
        String prefix = content.substring(0, objectStart + 1)
        String optionsBody = content.substring(objectStart + 1, objectEnd)
        String suffix = content.substring(objectEnd)

        String updatedOptionsBody = optionsBody.replaceFirst(
            /(?m)("BypassSSLValidation"\s*:\s*)(true|false)/,
            '$1' + bypassSslValidation
        )

        if (updatedOptionsBody == optionsBody) {
            int scanIndex = optionsBody.length() - 1
            while (scanIndex >= 0 && Character.isWhitespace(optionsBody.charAt(scanIndex))) {
                scanIndex--
            }

            boolean needsComma = (scanIndex >= 0 && optionsBody.charAt(scanIndex) != '{' as char && optionsBody.charAt(scanIndex) != ',' as char)
            String insertion = (needsComma ? "," : "") + newline + "    \"BypassSSLValidation\": " + bypassSslValidation
            updatedOptionsBody = optionsBody + insertion
        }

        String updatedContent = prefix + updatedOptionsBody + suffix

        // Remove accidental root-level copies if present.
        updatedContent = updatedContent.replaceFirst(
            /(?m)^[ \t]{0,2},?\s*"BypassSSLValidation"\s*:\s*(true|false)\s*,?\s*$/,
            ""
        )

        appSettingsFile.setText(updatedContent, "UTF-8")

        println("[DEBUG] Set PresenceOptions.BypassSSLValidation=" + bypassSslValidation + " in " + appSettingsFile.getAbsolutePath())
    }

    private int[] findNamedObjectBounds(String content, String objectName) {
        def matcher = content =~ /"${objectName}"\s*:\s*\{/
        if (!matcher.find()) {
            return null
        }

        int objectStart = content.indexOf('{', matcher.start())
        if (objectStart < 0) {
            return null
        }

        int depth = 0
        boolean inString = false
        boolean escaping = false

        for (int i = objectStart; i < content.length(); i++) {
            char ch = content.charAt(i)

            if (inString) {
                if (escaping) {
                    escaping = false
                }
                else if (ch == '\\' as char) {
                    escaping = true
                }
                else if (ch == '"' as char) {
                    inString = false
                }
                continue
            }

            if (ch == '"' as char) {
                inString = true
                continue
            }

            if (ch == '{' as char) {
                depth++
            }
            else if (ch == '}' as char) {
                depth--
                if (depth == 0) {
                    return [objectStart, i] as int[]
                }
            }
        }

        return null
    }

    private File findAppSettingsFile(File directory) {
        if (directory == null || !directory.exists()) {
            return null
        }

        if (directory.isFile()) {
            return directory.name.equalsIgnoreCase("appsettings.json") ? directory : null
        }

        File[] children = directory.listFiles()
        if (children == null) {
            return null
        }

        for (File child : children) {
            File match = findAppSettingsFile(child)
            if (match != null) {
                return match
            }
        }

        return null
    }

    private void stopRunningPresence() {
        println("[Action] Killing existing service...")

        if (isWindows) {
            killMatchingProcess(windowsPresenceExe)
            killMatchingProcess(windowsPresenceUpdaterExe)
        }

        killMatchingProcess(clientJar)
        killMatchingProcess(agentServiceJar)
    }

    private void killMatchingProcess(String processToMatch) {
        if (isWindows) {
            List<String> args
            if (processToMatch.toLowerCase().endsWith(".jar")) {
                args = [
                    "wmic",
                    "Path",
                    "win32_process",
                    "Where",
                    "CommandLine Like '%-jar%${processToMatch}%'",
                    "Call",
                    "Terminate"
                ]
            }
            else {
                args = ["taskkill", "/F", "/IM", processToMatch]
            }
            executeCommand(args)
        }
        else {
            println("[Action] Searching for processes matching ${processToMatch}.")
            List<String> args = ["pgrep", "-f", processToMatch]
            String out = executeCommand(args)

            out.eachLine {
                String pid = it
                println("[OK] Found matching PID to kill: ${pid}")
                args = ["kill", "-s", "15", pid]
                executeCommand(args)
            }
        }
    }

    private String executeCommand(List args) {
        println("[Action] Running command: '${args.join(' ')}'.")

        Process proc = args.execute()
        return waitForProcess(proc, (int)TimeUnit.SECONDS.toMillis(5))
    }

    private String waitForProcess(Process proc, int waitTime) {
        StringBuffer sout = new StringBuffer()
        StringBuffer serr = new StringBuffer()
        proc.waitForOrKill(waitTime)
        proc.consumeProcessOutput(sout, serr)

        return sout.toString()
    }

    private String waitForProcess(Process proc, int waitTime, boolean waitForSpawnOnly) {
        if (waitForSpawnOnly) {
            StringBuffer sout = new StringBuffer()
            StringBuffer serr = new StringBuffer()
            long sleepTime = Math.min(waitTime, (int)TimeUnit.SECONDS.toMillis(5))
            sleep(sleepTime)
            proc.consumeProcessOutput(sout, serr)

            return sout.toString()
        }

        return waitForProcess(proc, waitTime)
    }
}
