package com.ruoyi.generator.service.front;

import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

@Component
public class RunPreviewProcessRunner
{
    private static final String SPRING_BOOT_RUN_GOAL = "org.springframework.boot:spring-boot-maven-plugin:2.5.15:run";
    private static final int VITE_MIN_NODE_MAJOR = 18;
    private static final long NODE_VERSION_CHECK_TIMEOUT_SECONDS = 5L;
    private static final Pattern NODE_VERSION_PATTERN = Pattern.compile("v?(\\d+)(?:\\.\\d+){0,2}");
    private static final String WINDOWS_PROCESS_WRAPPER =
            "[System.IO.File]::WriteAllText("
                    + "$env:EASYCODE_PREVIEW_PID_FILE,"
                    + "[string]$PID,"
                    + "[System.Text.Encoding]::ASCII"
                    + ");"
                    + "& $env:ComSpec /d /s /c $env:EASYCODE_PREVIEW_COMMAND;"
                    + "exit $LASTEXITCODE";

    @Value("${easycode.preview.node-home:${EASYCODE_PREVIEW_NODE_HOME:}}")
    private String nodeHome;

    public Process startBackend(File workspace, Map<String, String> environment) throws IOException
    {
        return start("mvn spring-boot:run", workspace, environment);
    }

    public Process startBackend(File previewBuildDirectory, File rootPom, File reactorPom, String backendModulePath,
                                File localRepository, Map<String, String> environment) throws IOException
    {
        File localRepositoryParent = localRepository.getParentFile();
        if (localRepositoryParent != null && !localRepositoryParent.mkdirs() && !localRepositoryParent.isDirectory())
        {
            throw new IOException("Unable to create preview Maven repository directory");
        }
        String repositoryOption = shellQuote("-Dmaven.repo.local=" + localRepository.getAbsolutePath());
        String rootInstallCommand = "mvn " + repositoryOption + " -N -f " + shellQuote(rootPom.getAbsolutePath()) + " install";
        String backendRunCommand = "mvn " + repositoryOption + " -f " + shellQuote(reactorPom.getName())
                + " -pl " + shellQuote(backendModulePath) + " -am -DskipTests " + SPRING_BOOT_RUN_GOAL;
        return start(rootInstallCommand + " && " + backendRunCommand,
                previewBuildDirectory, environment);
    }

    public Process startFrontend(File vueDirectory, Map<String, String> environment) throws IOException
    {
        return start("npm install && npm run dev", vueDirectory, prepareFrontendEnvironment(vueDirectory, environment));
    }

    private Process start(String shellCommand, File workingDirectory, Map<String, String> environment) throws IOException
    {
        if (isWindows())
        {
            return startWindowsManaged(shellCommand, workingDirectory, environment);
        }
        ProcessBuilder builder = new ProcessBuilder(shellCommand(shellCommand));
        builder.directory(workingDirectory);
        builder.redirectErrorStream(true);
        if (environment != null)
        {
            builder.environment().putAll(environment);
        }
        return builder.start();
    }

    Process startCommandForTesting(String shellCommand, File workingDirectory,
                                   Map<String, String> environment) throws IOException
    {
        return start(shellCommand, workingDirectory, environment);
    }

    private Process startWindowsManaged(String shellCommand, File workingDirectory,
                                        Map<String, String> environment) throws IOException
    {
        File pidFile = new File(workingDirectory, ".easycode-process-" + UUID.randomUUID() + ".pid");
        ProcessBuilder builder = new ProcessBuilder(
                "powershell.exe",
                "-NoProfile",
                "-NonInteractive",
                "-ExecutionPolicy",
                "Bypass",
                "-Command",
                WINDOWS_PROCESS_WRAPPER);
        builder.directory(workingDirectory);
        builder.redirectErrorStream(true);
        if (environment != null)
        {
            builder.environment().putAll(environment);
        }
        builder.environment().put("EASYCODE_PREVIEW_COMMAND", shellCommand);
        builder.environment().put("EASYCODE_PREVIEW_PID_FILE", pidFile.getAbsolutePath());
        return new ManagedPreviewProcess(builder.start(), pidFile);
    }

    Map<String, String> prepareFrontendEnvironment(File vueDirectory, Map<String, String> environment) throws IOException
    {
        Map<String, String> result = new HashMap<String, String>();
        if (environment != null)
        {
            result.putAll(environment);
        }

        if (requiresModernNode(vueDirectory))
        {
            prepareModernNodeEnvironment(result);
        }
        else if (hasText(nodeHome))
        {
            prependNodeHome(result, normalizeNodeHome(nodeHome));
        }
        return result;
    }

    String pathEnvironmentKey()
    {
        String key = environmentKey(System.getenv());
        if (key != null)
        {
            return key;
        }
        return isWindows() ? "Path" : "PATH";
    }

    void setNodeHomeForTesting(String nodeHome)
    {
        this.nodeHome = nodeHome;
    }

    private void prepareModernNodeEnvironment(Map<String, String> environment) throws IOException
    {
        File configuredNodeHome = normalizeNodeHome(nodeHome);
        if (configuredNodeHome != null)
        {
            assertCompatibleNodeHome(configuredNodeHome);
            prependNodeHome(environment, configuredNodeHome);
            return;
        }

        NodeVersion currentVersion = readNodeVersion("node", environment);
        if (currentVersion != null && currentVersion.isAtLeast(VITE_MIN_NODE_MAJOR))
        {
            return;
        }

        File detectedNodeHome = findCompatibleFnmNodeHome();
        if (detectedNodeHome != null)
        {
            prependNodeHome(environment, detectedNodeHome);
            return;
        }

        String actualVersion = currentVersion == null ? "not found" : currentVersion.display;
        throw new IOException("Generated Vite frontend preview requires Node.js >= " + VITE_MIN_NODE_MAJOR
                + ", but current node is " + actualVersion
                + ". Install Node.js 18+ or set easycode.preview.node-home/EASYCODE_PREVIEW_NODE_HOME to the Node installation directory.");
    }

    private boolean requiresModernNode(File vueDirectory) throws IOException
    {
        File packageJsonFile = new File(vueDirectory, "package.json");
        if (!packageJsonFile.isFile())
        {
            return false;
        }

        String content = new String(Files.readAllBytes(packageJsonFile.toPath()), StandardCharsets.UTF_8);
        JSONObject packageJson = JSON.parseObject(content);
        return hasDependency(packageJson.getJSONObject("dependencies"), "vite")
                || hasDependency(packageJson.getJSONObject("devDependencies"), "vite")
                || hasViteScript(packageJson.getJSONObject("scripts"));
    }

    private boolean hasDependency(JSONObject dependencies, String dependencyName)
    {
        return dependencies != null && dependencies.containsKey(dependencyName);
    }

    private boolean hasViteScript(JSONObject scripts)
    {
        if (scripts == null)
        {
            return false;
        }
        for (String key : scripts.keySet())
        {
            String command = scripts.getString(key);
            if (command != null && command.contains("vite"))
            {
                return true;
            }
        }
        return false;
    }

    private void assertCompatibleNodeHome(File candidate) throws IOException
    {
        File nodeExecutable = nodeExecutable(candidate);
        if (nodeExecutable == null)
        {
            throw new IOException("Configured preview Node home does not contain a node executable: " + candidate.getAbsolutePath());
        }

        NodeVersion version = readNodeVersion(nodeExecutable.getAbsolutePath(), Collections.<String, String>emptyMap());
        if (version == null || !version.isAtLeast(VITE_MIN_NODE_MAJOR))
        {
            String actualVersion = version == null ? "not found" : version.display;
            throw new IOException("Generated Vite frontend preview requires Node.js >= " + VITE_MIN_NODE_MAJOR
                    + ", but configured node is " + actualVersion + " at " + candidate.getAbsolutePath());
        }
    }

    private File findCompatibleFnmNodeHome() throws IOException
    {
        String appData = System.getenv("APPDATA");
        if (!hasText(appData))
        {
            return null;
        }

        File nodeVersionsDirectory = new File(new File(appData, "fnm"), "node-versions");
        File[] versionDirectories = nodeVersionsDirectory.listFiles();
        if (versionDirectories == null)
        {
            return null;
        }

        File bestHome = null;
        NodeVersion bestVersion = null;
        for (File versionDirectory : versionDirectories)
        {
            File candidateHome = fnmInstallationDirectory(versionDirectory);
            if (candidateHome == null)
            {
                continue;
            }
            File executable = nodeExecutable(candidateHome);
            if (executable == null)
            {
                continue;
            }
            NodeVersion version = readNodeVersion(executable.getAbsolutePath(), Collections.<String, String>emptyMap());
            if (version != null && version.isAtLeast(VITE_MIN_NODE_MAJOR)
                    && (bestVersion == null || version.compareTo(bestVersion) > 0))
            {
                bestHome = candidateHome;
                bestVersion = version;
            }
        }
        return bestHome;
    }

    private File fnmInstallationDirectory(File versionDirectory)
    {
        if (versionDirectory == null || !versionDirectory.isDirectory() || !versionDirectory.getName().startsWith("v"))
        {
            return null;
        }
        File installationDirectory = new File(versionDirectory, "installation");
        return installationDirectory.isDirectory() ? installationDirectory : versionDirectory;
    }

    private NodeVersion readNodeVersion(String nodeCommand, Map<String, String> environment) throws IOException
    {
        ProcessBuilder builder = new ProcessBuilder(nodeCommand, "-v");
        builder.redirectErrorStream(true);
        if (environment != null)
        {
            builder.environment().putAll(environment);
        }

        Process process;
        try
        {
            process = builder.start();
        }
        catch (IOException e)
        {
            return null;
        }

        try
        {
            if (!process.waitFor(NODE_VERSION_CHECK_TIMEOUT_SECONDS, TimeUnit.SECONDS))
            {
                process.destroy();
                return null;
            }
        }
        catch (InterruptedException e)
        {
            Thread.currentThread().interrupt();
            return null;
        }

        StringBuilder output = new StringBuilder();
        BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8));
        try
        {
            String line;
            while ((line = reader.readLine()) != null)
            {
                output.append(line).append('\n');
            }
        }
        finally
        {
            reader.close();
        }

        if (process.exitValue() != 0)
        {
            return null;
        }
        return NodeVersion.parse(output.toString().trim());
    }

    private void prependNodeHome(Map<String, String> environment, File candidate)
    {
        if (candidate == null)
        {
            return;
        }
        String key = pathEnvironmentKey(environment);
        String current = environment.get(key);
        if (current == null)
        {
            current = environmentValue(System.getenv(), key);
        }
        String value = candidate.getAbsolutePath();
        if (hasText(current))
        {
            value += File.pathSeparator + current;
        }
        environment.put(key, value);
    }

    private String pathEnvironmentKey(Map<String, String> environment)
    {
        String key = environmentKey(environment);
        return key == null ? pathEnvironmentKey() : key;
    }

    private String environmentKey(Map<String, String> environment)
    {
        for (String key : environment.keySet())
        {
            if ("PATH".equalsIgnoreCase(key))
            {
                return key;
            }
        }
        return null;
    }

    private String environmentValue(Map<String, String> environment, String targetKey)
    {
        String value = environment.get(targetKey);
        if (value != null)
        {
            return value;
        }
        for (Map.Entry<String, String> entry : environment.entrySet())
        {
            if (entry.getKey().equalsIgnoreCase(targetKey))
            {
                return entry.getValue();
            }
        }
        return null;
    }

    private File normalizeNodeHome(String value)
    {
        if (!hasText(value))
        {
            return null;
        }
        File candidate = new File(value.trim());
        if (candidate.isFile())
        {
            return candidate.getParentFile();
        }
        return candidate;
    }

    private File nodeExecutable(File nodeHome)
    {
        String[] names = isWindows()
                ? new String[] { "node.exe", "node.cmd", "node.bat", "node" }
                : new String[] { "node" };
        for (String name : names)
        {
            File candidate = new File(nodeHome, name);
            if (candidate.isFile())
            {
                return candidate;
            }
        }
        return null;
    }

    private boolean hasText(String value)
    {
        return value != null && value.trim().length() > 0;
    }

    private List<String> shellCommand(String command)
    {
        List<String> result = new ArrayList<String>();
        if (isWindows())
        {
            result.add("cmd");
            result.add("/c");
        }
        else
        {
            result.add("sh");
            result.add("-c");
        }
        result.add(command);
        return result;
    }

    private String shellQuote(String value)
    {
        return "\"" + value.replace("\"", "\\\"") + "\"";
    }

    private boolean isWindows()
    {
        String osName = System.getProperty("os.name");
        return osName != null && osName.toLowerCase().contains("win");
    }

    private static class NodeVersion
    {
        private final String display;
        private final int major;
        private final int minor;
        private final int patch;

        private NodeVersion(String display, int major, int minor, int patch)
        {
            this.display = display;
            this.major = major;
            this.minor = minor;
            this.patch = patch;
        }

        private static NodeVersion parse(String output)
        {
            Matcher matcher = NODE_VERSION_PATTERN.matcher(output);
            if (!matcher.find())
            {
                return null;
            }
            String version = matcher.group();
            String[] parts = version.replace("v", "").split("\\.");
            return new NodeVersion(version, parsePart(parts, 0), parsePart(parts, 1), parsePart(parts, 2));
        }

        private static int parsePart(String[] parts, int index)
        {
            if (parts.length <= index)
            {
                return 0;
            }
            try
            {
                return Integer.parseInt(parts[index]);
            }
            catch (NumberFormatException e)
            {
                return 0;
            }
        }

        private boolean isAtLeast(int expectedMajor)
        {
            return major >= expectedMajor;
        }

        private int compareTo(NodeVersion other)
        {
            if (major != other.major)
            {
                return major - other.major;
            }
            if (minor != other.minor)
            {
                return minor - other.minor;
            }
            return patch - other.patch;
        }
    }
}
