package p.studio.compiler.integration;

import p.studio.compiler.backend.bytecode.BytecodeModule;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;

final class RuntimeBackedCompatibilityAdapter implements RuntimeCompatibilityAdapter {
    private static final String DEFAULT_RUNTIME_LINE = "runtime-line-unknown";
    private static final String FALLBACK_MODE = "local-fallback";
    private static final String STRICT_FLAG_ENV = "PROMETEU_GATE_I_STRICT";

    private final RuntimeCompatibilityAdapter fallback;
    private final Path runtimeRoot;
    private final String runtimeLine;
    private final String runtimeCheckCommand;
    private final boolean strictRuntimeBacked;

    RuntimeBackedCompatibilityAdapter(final RuntimeCompatibilityAdapter fallback) {
        this(
                fallback == null ? new LocalRuntimeCompatibilityAdapter() : fallback,
                Path.of("../runtime"),
                System.getenv("PROMETEU_RUNTIME_CHECK_CMD"),
                strictRuntimeBackedFromEnvironment());
    }

    RuntimeBackedCompatibilityAdapter(
            final RuntimeCompatibilityAdapter fallback,
            final Path runtimeRoot,
            final String runtimeCheckCommand) {
        this(fallback, runtimeRoot, runtimeCheckCommand, false);
    }

    RuntimeBackedCompatibilityAdapter(
            final RuntimeCompatibilityAdapter fallback,
            final Path runtimeRoot,
            final String runtimeCheckCommand,
            final boolean strictRuntimeBacked) {
        this.fallback = fallback == null ? new LocalRuntimeCompatibilityAdapter() : fallback;
        this.runtimeRoot = runtimeRoot == null ? Path.of("../runtime") : runtimeRoot;
        this.runtimeLine = readRuntimeLine(this.runtimeRoot);
        this.runtimeCheckCommand = runtimeCheckCommand == null ? "" : runtimeCheckCommand.trim();
        this.strictRuntimeBacked = strictRuntimeBacked;
    }

    @Override
    public CompatibilityResult check(final BytecodeModule module) {
        final var local = fallback.check(module);
        if (runtimeCheckCommand.isBlank()) {
            return fallbackWithReason(local, "runtime command not configured");
        }
        if (!Files.isDirectory(runtimeRoot)) {
            return fallbackWithReason(local, "runtime root is unavailable: " + runtimeRoot);
        }

        Path artifact = null;
        try {
            artifact = Files.createTempFile("gate-i-runtime-", ".pbx");
            Files.write(artifact, module.serialize());
            final var command = renderCommand(runtimeCheckCommand, artifact);
            final var process = new ProcessBuilder(List.of("sh", "-lc", command))
                    .directory(runtimeRoot.toFile())
                    .redirectErrorStream(true)
                    .start();
            final var output = new String(process.getInputStream().readAllBytes(), StandardCharsets.UTF_8).trim();
            final var exitCode = process.waitFor();
            if (exitCode != 0) {
                return new CompatibilityResult(
                        CompatibilityError.RUNTIME_REJECTED,
                        local.hostcallCount(),
                        runtimeLine,
                        "runtime-backed",
                        "runtime command rejected artifact (exit=%d): %s".formatted(exitCode, shrink(output)));
            }
            return new CompatibilityResult(
                    local.error(),
                    local.hostcallCount(),
                    runtimeLine,
                    "runtime-backed",
                    "runtime command accepted artifact");
        } catch (IOException | InterruptedException e) {
            if (e instanceof InterruptedException) {
                Thread.currentThread().interrupt();
            }
            return fallbackWithReason(local, "runtime command failed: " + e.getMessage());
        } finally {
            if (artifact != null) {
                try {
                    Files.deleteIfExists(artifact);
                } catch (IOException ignored) {
                }
            }
        }
    }

    private CompatibilityResult fallbackWithReason(
            final CompatibilityResult local,
            final String reason) {
        if (strictRuntimeBacked) {
            return new CompatibilityResult(
                    CompatibilityError.RUNTIME_BACKED_REQUIRED,
                    local.hostcallCount(),
                    runtimeLine,
                    FALLBACK_MODE,
                    "strict gate-i requires runtime-backed adapter: " + reason);
        }
        return new CompatibilityResult(
                local.error(),
                local.hostcallCount(),
                runtimeLine,
                FALLBACK_MODE,
                reason);
    }

    private String renderCommand(
            final String commandTemplate,
            final Path artifactPath) {
        final var quotedPath = shellQuote(artifactPath.toAbsolutePath().toString());
        if (commandTemplate.contains("{artifact}")) {
            return commandTemplate.replace("{artifact}", quotedPath);
        }
        return commandTemplate + " " + quotedPath;
    }

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

    private static String readRuntimeLine(final Path runtimeRoot) {
        final var versionFile = runtimeRoot.resolve("VERSION.txt");
        if (!Files.exists(versionFile)) {
            return DEFAULT_RUNTIME_LINE;
        }
        try {
            final var content = Files.readString(versionFile).trim();
            if (content.isBlank()) {
                return DEFAULT_RUNTIME_LINE;
            }
            return "runtime-v" + content;
        } catch (IOException e) {
            return DEFAULT_RUNTIME_LINE;
        }
    }

    private String shrink(final String output) {
        if (output == null || output.isBlank()) {
            return "<no output>";
        }
        if (output.length() <= 160) {
            return output;
        }
        return output.substring(0, 160) + "...";
    }

    private static boolean strictRuntimeBackedFromEnvironment() {
        if (isTruthy(System.getenv(STRICT_FLAG_ENV))) {
            return true;
        }
        return isTruthy(System.getenv("CI"));
    }

    private static boolean isTruthy(final String value) {
        if (value == null) {
            return false;
        }
        return switch (value.trim().toLowerCase()) {
            case "1", "true", "yes", "on" -> true;
            default -> false;
        };
    }
}
