package p.studio.compiler.backend.irvm;

import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
import java.util.regex.Pattern;

import static org.junit.jupiter.api.Assertions.*;

@Disabled
class IRVMIntrinsicRegistryParityTest {
    private static final String VM_ARCH_INTRINSICS_PATH = "docs/vm-arch/INTRINSICS.csv";
    private static final String RUNTIME_INTRINSICS_PATH = "crates/console/prometeu-vm/src/builtins.rs";
    private static final Pattern OWNER_PATTERN = Pattern.compile("owner:\\s*\"([^\"]+)\",");
    private static final Pattern NAME_PATTERN = Pattern.compile("name:\\s*\"([^\"]+)\",");
    private static final Pattern VERSION_PATTERN = Pattern.compile("version:\\s*(\\d+),");
    private static final Pattern ID_PATTERN = Pattern.compile("id:\\s*(0x[0-9A-Fa-f]+|\\d+),");
    private static final String STRICT_PARITY_ENV = "PROMETEU_INTRINSIC_PARITY_STRICT";

    @Test
    void registryDefinitionsMustExposeExpectedCanonicalEntries() {
        final var snapshot = IRVMIntrinsicRegistry.snapshotByCanonicalIdentity();
        assertFalse(snapshot.isEmpty());
        assertEquals(23, snapshot.size());
        assertEquals(0x2000, snapshot.get("input.pad@1"));
        assertEquals(0x2033, snapshot.get("input.button.hold@1"));
    }

    @Test
    void registryDefinitionsMustStayInSyncWithVmArchCsv() throws IOException {
        final var specPath = locateRepoRoot().resolve(VM_ARCH_INTRINSICS_PATH);
        assertTrue(Files.isRegularFile(specPath), "intrinsics spec is missing: " + specPath);

        final var expected = parseVmArchIntrinsicRows(specPath);
        final var actual = Arrays.stream(IRVMIntrinsicDefinition.values())
                .map(IntrinsicCsvRow::fromDefinition)
                .toList();
        assertEquals(expected, actual);
    }

    @Test
    void registryMustStayInSyncWithRuntimeBuiltinsTable() throws IOException {
        final var compilerSnapshot = IRVMIntrinsicRegistry.snapshotByCanonicalIdentity();
        final var runtimeIntrinsicsFile = locateRuntimeIntrinsicsFile();
        if (!Files.exists(runtimeIntrinsicsFile)) {
            if (strictParityEnabled()) {
                fail("runtime intrinsic table not found: " + runtimeIntrinsicsFile.toAbsolutePath());
            }
            return;
        }
        final var runtimeSnapshot = parseRuntimeIntrinsicSnapshot(runtimeIntrinsicsFile);
        assertEquals(runtimeSnapshot, compilerSnapshot, parityDiff(runtimeSnapshot, compilerSnapshot));
    }

    private List<IntrinsicCsvRow> parseVmArchIntrinsicRows(final Path specPath) throws IOException {
        final var lines = Files.readAllLines(specPath, StandardCharsets.UTF_8);
        final var rows = new ArrayList<IntrinsicCsvRow>();
        for (int index = 0; index < lines.size(); index++) {
            final var rawLine = lines.get(index);
            final var line = rawLine.trim();
            if (line.isBlank()) {
                continue;
            }
            if (index == 0) {
                assertEquals(
                        "final_id_hex,final_id_dec,canonical_name,canonical_version,owner,name,arg_slots,ret_slots,arg_layout,ret_layout,deterministic,may_allocate",
                        line,
                        "unexpected intrinsic csv header");
                continue;
            }
            final var columns = line.split(",", -1);
            if (columns.length != 12) {
                fail("invalid intrinsics csv row at line " + (index + 1) + ": " + rawLine);
            }
            rows.add(new IntrinsicCsvRow(
                    normalizeHexId(columns[0]),
                    parseUnsignedInt(columns[1], "final_id_dec", index + 1),
                    requireNonBlank(columns[2], "canonical_name", index + 1),
                    parseLong(columns[3], "canonical_version", index + 1),
                    requireNonBlank(columns[4], "owner", index + 1),
                    requireNonBlank(columns[5], "name", index + 1),
                    parseInt(columns[6], "arg_slots", index + 1),
                    parseInt(columns[7], "ret_slots", index + 1),
                    columns[8].trim(),
                    columns[9].trim(),
                    parseBoolean(columns[10], "deterministic", index + 1),
                    parseBoolean(columns[11], "may_allocate", index + 1)));
        }
        return List.copyOf(rows);
    }

    private Map<String, Integer> parseRuntimeIntrinsicSnapshot(final Path runtimeFile) throws IOException {
        final var runtimeSource = Files.readAllLines(runtimeFile, StandardCharsets.UTF_8);
        final var mapped = new LinkedHashMap<String, Integer>();
        var insideIntrinsicsArray = false;
        var insideEntry = false;
        String owner = null;
        String name = null;
        Long version = null;
        Integer id = null;
        for (final var rawLine : runtimeSource) {
            final var line = rawLine.trim();
            if (!insideIntrinsicsArray) {
                if (line.startsWith("const INTRINSICS:")) {
                    insideIntrinsicsArray = true;
                }
                continue;
            }
            if (line.equals("];")) {
                break;
            }
            if (line.startsWith("IntrinsicMeta {")) {
                insideEntry = true;
                owner = null;
                name = null;
                version = null;
                id = null;
                continue;
            }
            if (!insideEntry) {
                continue;
            }
            final var ownerMatcher = OWNER_PATTERN.matcher(line);
            if (ownerMatcher.find()) {
                owner = ownerMatcher.group(1);
            }
            final var nameMatcher = NAME_PATTERN.matcher(line);
            if (nameMatcher.find()) {
                name = nameMatcher.group(1);
            }
            final var versionMatcher = VERSION_PATTERN.matcher(line);
            if (versionMatcher.find()) {
                version = Long.parseLong(versionMatcher.group(1));
            }
            final var idMatcher = ID_PATTERN.matcher(line);
            if (idMatcher.find()) {
                id = parseRuntimeId(idMatcher.group(1));
            }
            if (line.equals("},")) {
                if (owner == null || name == null || version == null || id == null) {
                    throw new IllegalStateException("invalid runtime intrinsic entry near line: " + rawLine);
                }
                final var identity = owner + "." + name + "@" + version;
                final var previous = mapped.putIfAbsent(identity, id);
                if (previous != null && previous != id) {
                    throw new IllegalStateException(
                            "runtime intrinsic identity collision for " + identity + ": " + previous + " vs " + id);
                }
                insideEntry = false;
            }
        }
        return Map.copyOf(mapped);
    }

    private Path locateRepoRoot() {
        var cursor = Path.of(System.getProperty("user.dir")).toAbsolutePath().normalize();
        while (cursor != null) {
            if (Files.isDirectory(cursor.resolve("docs"))
                    && Files.isDirectory(cursor.resolve("prometeu-compiler"))
                    && Files.exists(cursor.resolve("settings.gradle.kts"))) {
                return cursor;
            }
            cursor = cursor.getParent();
        }
        fail("could not locate repository root from working directory");
        throw new IllegalStateException("unreachable");
    }

    private Path locateRuntimeIntrinsicsFile() {
        return locateRepoRoot()
                .getParent()
                .resolve("runtime")
                .resolve(RUNTIME_INTRINSICS_PATH);
    }

    private int parseRuntimeId(final String rawId) {
        if (rawId.startsWith("0x") || rawId.startsWith("0X")) {
            return Integer.parseUnsignedInt(rawId.substring(2), 16);
        }
        return Integer.parseUnsignedInt(rawId, 10);
    }

    private String requireNonBlank(
            final String value,
            final String fieldName,
            final int lineNumber) {
        final var normalized = value.trim();
        if (normalized.isBlank()) {
            fail("blank " + fieldName + " at line " + lineNumber);
        }
        return normalized;
    }

    private int parseInt(
            final String rawValue,
            final String fieldName,
            final int lineNumber) {
        try {
            return Integer.parseInt(rawValue.trim());
        } catch (NumberFormatException ex) {
            fail("invalid " + fieldName + " at line " + lineNumber + ": " + rawValue);
            throw new IllegalStateException("unreachable", ex);
        }
    }

    private long parseLong(
            final String rawValue,
            final String fieldName,
            final int lineNumber) {
        try {
            return Long.parseLong(rawValue.trim());
        } catch (NumberFormatException ex) {
            fail("invalid " + fieldName + " at line " + lineNumber + ": " + rawValue);
            throw new IllegalStateException("unreachable", ex);
        }
    }

    private int parseUnsignedInt(
            final String rawValue,
            final String fieldName,
            final int lineNumber) {
        try {
            return Integer.parseUnsignedInt(rawValue.trim(), 10);
        } catch (NumberFormatException ex) {
            fail("invalid " + fieldName + " at line " + lineNumber + ": " + rawValue);
            throw new IllegalStateException("unreachable", ex);
        }
    }

    private boolean parseBoolean(
            final String rawValue,
            final String fieldName,
            final int lineNumber) {
        return switch (rawValue.trim().toLowerCase(Locale.ROOT)) {
            case "true" -> true;
            case "false" -> false;
            default -> {
                fail("invalid " + fieldName + " at line " + lineNumber + ": " + rawValue);
                yield false;
            }
        };
    }

    private String normalizeHexId(final String rawValue) {
        final var trimmed = rawValue.trim();
        final var parsed = parseRuntimeId(trimmed);
        return "0x" + Integer.toUnsignedString(parsed, 16).toUpperCase(Locale.ROOT);
    }

    private boolean strictParityEnabled() {
        final var strict = System.getenv(STRICT_PARITY_ENV);
        if (isTruthy(strict)) {
            return true;
        }
        return false;
    }

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

    private String parityDiff(
            final Map<String, Integer> runtimeSnapshot,
            final Map<String, Integer> compilerSnapshot) {
        final var missingInCompiler = new ArrayList<String>();
        final var missingInRuntime = new ArrayList<String>();
        final var mismatchedIds = new ArrayList<String>();
        for (final var runtimeEntry : runtimeSnapshot.entrySet()) {
            final var compilerId = compilerSnapshot.get(runtimeEntry.getKey());
            if (compilerId == null) {
                missingInCompiler.add(runtimeEntry.getKey());
                continue;
            }
            if (!runtimeEntry.getValue().equals(compilerId)) {
                mismatchedIds.add(runtimeEntry.getKey() + " runtime=" + runtimeEntry.getValue() + " compiler=" + compilerId);
            }
        }
        for (final var compilerEntry : compilerSnapshot.entrySet()) {
            if (!runtimeSnapshot.containsKey(compilerEntry.getKey())) {
                missingInRuntime.add(compilerEntry.getKey());
            }
        }
        return "intrinsic registry parity mismatch"
                + "\nmissingInCompiler=" + List.copyOf(missingInCompiler)
                + "\nmissingInRuntime=" + List.copyOf(missingInRuntime)
                + "\nidMismatches=" + List.copyOf(mismatchedIds);
    }

    private record IntrinsicCsvRow(
            String finalIdHex,
            int finalIdDec,
            String canonicalName,
            long canonicalVersion,
            String owner,
            String name,
            int argSlots,
            int retSlots,
            String argLayout,
            String retLayout,
            boolean deterministic,
            boolean mayAllocate) {

        private static IntrinsicCsvRow fromDefinition(final IRVMIntrinsicDefinition definition) {
            return new IntrinsicCsvRow(
                    "0x" + Integer.toUnsignedString(definition.finalId(), 16).toUpperCase(Locale.ROOT),
                    definition.finalId(),
                    definition.canonicalName(),
                    definition.canonicalVersion(),
                    definition.owner(),
                    definition.intrinsicName(),
                    definition.argSlots(),
                    definition.retSlots(),
                    definition.argLayout(),
                    definition.retLayout(),
                    definition.deterministic(),
                    definition.mayAllocate());
        }
    }
}
