<?php

/**
 * Resolves PHP/ValaIR call names to generated Vala runtime wrapper calls.
 *
 * The resolver derives runtime wrapper names using the `phpfunc_*` convention
 * and validates that the derived wrapper exists in a runtime VAPI. Core wrappers
 * live in PhpefRuntimeCore; Zend/embed-backed wrappers live in PhpefRuntimeEmbed.
 */
final class Php2ValaFuncResolver
{
    // runtime wrapper => metadata parsed from VAPI, including min/max arity and signature types
    private array $runtimeFunctions;
    // normalized PHP function name => ordered runtime wrapper candidates, including signature-stable typed fast paths
    private array $runtimeFunctionCandidates;
    // normalized PHP function name => extension name => true
    private array $excludedRuntimeFunctions;
    // normalized PHP function name => zendcall-only metadata from runtime VAPI comments
    private array $zendcallOnlyRuntimeFunctions;
    private array $usedNamespaces = [];

    /**
     * Create a resolver from runtime VAPI metadata.
     */
    public function __construct(?string $runtimeCoreVapiPath = null, ?string $runtimeEmbedVapiPath = null, ?array $runtimeNativeVapiPaths = null)
    {
        $coreVapiPath = $runtimeCoreVapiPath ?? self::defaultRuntimeCoreVapiPath();
        $embedVapiPath = $runtimeEmbedVapiPath ?? self::defaultRuntimeEmbedVapiPath();
        $nativeVapiPaths = $runtimeNativeVapiPaths ?? self::defaultRuntimeNativeVapiPaths();
        $coreRuntimeFunctions = self::loadRuntimeFunctions($coreVapiPath, "PhpefRuntimeCore", true);
        $embedRuntimeFunctions = self::loadRuntimeFunctions($embedVapiPath, "PhpefRuntimeEmbed", false);
        $nativeRuntimeFunctionSets = [];
        foreach ($nativeVapiPaths as $nativeVapiPath) {
            if (!is_string($nativeVapiPath) || $nativeVapiPath === '') {
                continue;
            }
            $nativeRuntimeFunctions = self::loadRuntimeFunctions(
                $nativeVapiPath,
                self::runtimeNamespaceFromVapiPath($nativeVapiPath),
                false,
            );
            if ($nativeRuntimeFunctions !== []) {
                $nativeRuntimeFunctionSets[] = $nativeRuntimeFunctions;
            }
        }

        $this->runtimeFunctions = [];
        $this->runtimeFunctions += $coreRuntimeFunctions;
        $this->runtimeFunctions += $embedRuntimeFunctions;
        foreach ($nativeRuntimeFunctionSets as $nativeRuntimeFunctions) {
            $this->runtimeFunctions += $nativeRuntimeFunctions;
        }

        $this->runtimeFunctionCandidates = [];
        $this->indexRuntimeFunctionCandidates($coreRuntimeFunctions);
        $this->indexRuntimeFunctionCandidates($embedRuntimeFunctions);
        foreach ($nativeRuntimeFunctionSets as $nativeRuntimeFunctions) {
            $this->indexRuntimeFunctionCandidates($nativeRuntimeFunctions);
        }
        $this->excludedRuntimeFunctions = [];
        $this->mergeExcludedRuntimeFunctions(self::loadExcludedRuntimeFunctions($coreVapiPath));
        $this->mergeExcludedRuntimeFunctions(self::loadExcludedRuntimeFunctions($embedVapiPath));

        $this->zendcallOnlyRuntimeFunctions = [];
        $this->mergeZendcallOnlyRuntimeFunctions(self::loadZendcallOnlyRuntimeFunctions($coreVapiPath));
        $this->mergeZendcallOnlyRuntimeFunctions(self::loadZendcallOnlyRuntimeFunctions($embedVapiPath));
    }

    /**
     * Create a resolver using environment/default VAPI discovery.
     */
    public static function default(): self
    {
        return new self();
    }

    /**
     * Resolve one PHP/ValaIR call name to a runtime wrapper function.
     */
    public function resolveRuntimeFunction(string $phpCallName, ?int $argumentCount = null): string
    {
        $resolution = $this->runtimeFunctionResolution($phpCallName, $argumentCount);
        if (!$resolution['found']) {
            $message = 'Could not resolve PHP call "' . $phpCallName . '" to runtime wrapper "' . $resolution['runtimeName'] . '"; wrapper is missing from known phpef runtime VAPIs';
            $existsInSelectedPhpRuntime = function_exists($phpCallName);
            $message .= '; selected PHP runtime reports function_exists("' . $phpCallName . '") = ' . ($existsInSelectedPhpRuntime ? 'true' : 'false');
            if ($existsInSelectedPhpRuntime) {
                $message .= '; PHP function exists, so this is a PHPEF implementation gap: add a phpfunc_* wrapper/lowering or choose an explicit zendcall_* escape hatch when appropriate';
            } else {
                $message .= '; this function is not available in the selected PHP runtime/extension set';
            }
            $zendcallOnly = $this->zendcallOnlyRuntimeFunctionForCall($phpCallName);
            if ($zendcallOnly !== null) {
                $message .= '; available as explicit "'
                    . $zendcallOnly['callableName'] . '()" / wrapper "' . $zendcallOnly['wrapperName'] . '"';
                $message .= '; CAUTION: zendcall_* calls directly into the selected libphp.so/Zend runtime via phpef-runtime-embed. It is an explicit escape hatch, not a PHPEF phpfunc_* implementation, so behavior/results are not comparable to a normal PHPEF-implemented wrapper';
                if ($zendcallOnly['reason'] !== '') {
                    $message .= '; reason from runtime VAPI: ' . $zendcallOnly['reason'];
                }
            }
            $excludedExtensions = $this->excludedRuntimeExtensionsForCall($phpCallName);
            if ($excludedExtensions !== []) {
                $message .= '; function exists in PHP ' . self::formatExtensionList($excludedExtensions)
                    . ', but that extension/function is not enabled/included in the selected phpef runtime VAPI';
            }
            throw new RuntimeException($message);
        }

        if (!$resolution['arityMatches']) {
            throw new RuntimeException(
                'Could not resolve PHP call "' . $phpCallName . '" to runtime wrapper "' . $resolution['runtimeName'] . '"; wrapper expects '
                . self::formatArgumentRange($resolution['minArgs'], $resolution['maxArgs']) . ' argument(s), got ' . $argumentCount
            );
        }

        $this->usedNamespaces[$resolution['namespace']] = true;
        return self::runtimeCalleeName($resolution['runtimeName'], $resolution['namespace']);
    }

    private static function runtimeCalleeName(string $runtimeName, string $namespace): string
    {
        if ($namespace === 'PhpefRuntimeCore') {
            return $runtimeName;
        }
        return $namespace . '.' . $runtimeName;
    }

    /**
     * Return whether a PHP/ValaIR call has a known runtime wrapper.
     */
    public function hasRuntimeFunction(string $phpCallName, ?int $argumentCount = null): bool
    {
        $resolution = $this->runtimeFunctionResolution($phpCallName, $argumentCount);
        return $resolution['found'] && $resolution['arityMatches'];
    }

    /**
     * Return runtime wrapper candidates for a PHP function.
     * Base wrapper comes first when present; signature-stable typed fast paths
     * (`phpfunc_<name>_typed_<return>_arg1_<type>...`) are grouped with the
     * same PHP base name and include parsed name/VAPI signature data.
     */
    public function resolveRuntimeFunctionCandidates(string $phpCallName, ?int $argumentCount = null): array
    {
        $normalizedName = $this->normalizeCallName($phpCallName);
        $candidates = $this->runtimeFunctionCandidates[$normalizedName] ?? [];
        if ($argumentCount === null) {
            return $candidates;
        }

        $matching = [];
        foreach ($candidates as $candidate) {
            if (self::argumentCountMatches($argumentCount, $candidate['minArgs'], $candidate['maxArgs'])) {
                $matching[] = $candidate;
            }
        }
        return $matching;
    }

    /**
     * Resolve a typed runtime fast path from metadata, returning the full Vala callee and call policy.
     *
     * @param string $phpCallName PHP-level function or construct name, without the phpfunc_ prefix.
     * @param string[] $argumentTypes Normalized PHP-level argument types, e.g. string, int, bool:true.
     * @param string[] $allowedKinds Allowed typed_fast_path metadata values.
     * @param string|null $returnType Optional normalized return type filter.
     * @param string|null $requiredNamespace Optional runtime namespace filter to keep typed rewrites within the current runtime.
     * @return array{callee:string,runtimeName:string,namespace:string,typedSignature:array,runtimeMetadata:array,passArguments:bool}|null Matching fast path descriptor, or null when none is safe.
     */
    public function resolveTypedFastPath(string $phpCallName, array $argumentTypes, array $allowedKinds = ['static_php_call', 'native'], ?string $returnType = null, ?string $requiredNamespace = null): ?array
    {
        foreach ($this->resolveRuntimeFunctionCandidates($phpCallName) as $candidate) {
            if ($requiredNamespace !== null && ($candidate['namespace'] ?? null) !== $requiredNamespace) {
                continue;
            }
            $runtimeMetadata = $candidate['runtimeMetadata'] ?? [];
            if (!is_array($runtimeMetadata) || !in_array($runtimeMetadata['typed_fast_path'] ?? null, $allowedKinds, true)) {
                continue;
            }
            $typedSignature = $candidate['typedSignature'] ?? null;
            if (!is_array($typedSignature)) {
                continue;
            }
            if (($typedSignature['argumentTypes'] ?? null) !== $argumentTypes) {
                continue;
            }
            if ($returnType !== null && ($typedSignature['returnType'] ?? null) !== $returnType) {
                continue;
            }
            $callee = $candidate['callee'] ?? null;
            if (!is_string($callee) || $callee === '') {
                continue;
            }
            return [
                'callee' => $callee,
                'runtimeName' => $candidate['runtimeName'] ?? '',
                'namespace' => $candidate['namespace'] ?? '',
                'typedSignature' => $typedSignature,
                'runtimeMetadata' => $runtimeMetadata,
                'passArguments' => self::typedFastPathPassesArguments($candidate, $runtimeMetadata),
            ];
        }

        return null;
    }

    private function runtimeFunctionResolution(string $phpCallName, ?int $argumentCount = null): array
    {
        $normalizedName = $this->normalizeCallName($phpCallName);
        $runtimeName = 'phpfunc_' . $normalizedName;

        if (!isset($this->runtimeFunctions[$runtimeName])) {
            return [
                'found' => false,
                'arityMatches' => false,
                'runtimeName' => $runtimeName,
                'namespace' => '',
                'minArgs' => 0,
                'maxArgs' => 0,
            ];
        }

        $metadata = $this->runtimeFunctions[$runtimeName];
        return [
            'found' => true,
            'arityMatches' => $argumentCount === null || self::argumentCountMatches($argumentCount, $metadata['minArgs'], $metadata['maxArgs']),
            'runtimeName' => $runtimeName,
            'namespace' => $metadata['namespace'],
            'minArgs' => $metadata['minArgs'],
            'maxArgs' => $metadata['maxArgs'],
        ];
    }

    /**
     * Return Vala namespaces used while resolving calls.
     */
    public function usedNamespaces(): array
    {
        return array_values(array_filter(
            array_keys($this->usedNamespaces),
            static fn (string $namespace): bool => $namespace === 'PhpefRuntimeCore',
        ));
    }

    private function indexRuntimeFunctionCandidates(array $functions): void
    {
        foreach ($functions as $functionName => $metadata) {
            if (!str_starts_with($functionName, 'phpfunc_')) {
                continue;
            }

            $typedName = self::parseTypedRuntimeName($functionName);
            $runtimeMetadata = is_array($metadata['runtimeMetadata'] ?? null) ? $metadata['runtimeMetadata'] : [];
            $metadataPhpName = isset($runtimeMetadata['php_name']) && is_string($runtimeMetadata['php_name']) ? strtolower($runtimeMetadata['php_name']) : null;
            $baseRuntimeName = $metadataPhpName !== null && $metadataPhpName !== '' ? 'phpfunc_' . $metadataPhpName : ($typedName === null ? $functionName : 'phpfunc_' . $typedName['phpName']);
            $phpName = substr($baseRuntimeName, strlen('phpfunc_'));
            $metadataTypedSignature = self::typedSignatureFromRuntimeMetadata($runtimeMetadata);
            if ($typedName !== null && !isset($runtimeMetadata['typed_fast_path']) && ($metadata['namespace'] ?? '') === 'PhpefRuntimeEmbed') {
                $runtimeMetadata['typed_fast_path'] = 'native';
            }

            $candidate = $metadata;
            $candidate['runtimeName'] = $functionName;
            $candidate['baseRuntimeName'] = $baseRuntimeName;
            $candidate['phpName'] = $phpName;
            $candidate['signature'] = [
                'returnType' => $metadata['returnType'],
                'parameters' => $metadata['parameters'],
            ];
            $candidate['callee'] = self::runtimeCalleeName($functionName, (string) ($metadata['namespace'] ?? ''));
            $candidate['typedSignature'] = $metadataTypedSignature ?? ($typedName === null ? null : [
                'returnType' => $typedName['returnType'],
                'argumentTypes' => $typedName['argumentTypes'],
            ]);
            $candidate['typedSignatureSuffix'] = $typedName['suffix'] ?? null;
            // Small metadata passthrough for optimizer selection.  A typed name
            // alone only describes a callable signature; whether it is a PHPEF
            // fast/static boundary is intentionally decided from RuntimeEmbed
            // VAPI comments such as "typed_fast_path static_php_call"/"native".
            $candidate['runtimeMetadata'] = $runtimeMetadata;
            $this->runtimeFunctionCandidates[$phpName][] = $candidate;
        }
    }

    private static function defaultRuntimeCoreVapiPath(): string
    {
        $explicit = getenv('PHPEF_RUNTIME_CORE_VAPI');
        if (is_string($explicit) && $explicit !== '') {
            return $explicit;
        }

        $vapidir = getenv('PHPEF_RUNTIME_VAPIDIR');
        if (is_string($vapidir) && $vapidir !== '') {
            return rtrim($vapidir, '/') . '/phpef-runtime-core.vapi';
        }

        foreach ([
            '/usr/share/vala/vapi/phpef-runtime-core.vapi',
            '/usr/local/share/vala/vapi/phpef-runtime-core.vapi',
        ] as $candidate) {
            if (is_file($candidate)) {
                return $candidate;
            }
        }

        return '/usr/share/vala/vapi/phpef-runtime-core.vapi';
    }

    private static function defaultRuntimeEmbedVapiPath(): string
    {
        $explicit = getenv('PHPEF_RUNTIME_EMBED_VAPI');
        if (is_string($explicit) && $explicit !== '') {
            return $explicit;
        }

        $vapidir = getenv('PHPEF_RUNTIME_VAPIDIR');
        if (is_string($vapidir) && $vapidir !== '') {
            return rtrim($vapidir, '/') . '/phpef-runtime-embed.vapi';
        }

        foreach ([
            '/usr/share/vala/vapi/phpef-runtime-embed.vapi',
            '/usr/local/share/vala/vapi/phpef-runtime-embed.vapi',
        ] as $candidate) {
            if (is_file($candidate)) {
                return $candidate;
            }
        }

        return '/usr/share/vala/vapi/phpef-runtime-embed.vapi';
    }


    private static function defaultRuntimeNativeVapiPaths(): array
    {
        $paths = [];

        $explicitList = getenv('PHPEF_RUNTIME_NATIVE_VAPIS');
        if (is_string($explicitList) && $explicitList !== '') {
            foreach (explode(PATH_SEPARATOR, $explicitList) as $path) {
                if ($path !== '') {
                    $paths[] = $path;
                }
            }
        }

        $vapidir = getenv('PHPEF_RUNTIME_VAPIDIR');
        if (is_string($vapidir) && $vapidir !== '') {
            foreach (glob(rtrim($vapidir, '/') . '/phpef-runtime-native-*.vapi') ?: [] as $path) {
                $paths[] = $path;
            }
        }

        foreach (['/usr/share/vala/vapi', '/usr/local/share/vala/vapi'] as $dir) {
            foreach (glob($dir . '/phpef-runtime-native-*.vapi') ?: [] as $path) {
                $paths[] = $path;
            }
        }

        $existing = [];
        foreach (array_values(array_unique($paths)) as $path) {
            if (is_file($path)) {
                $existing[] = $path;
            }
        }
        sort($existing, SORT_STRING);
        return $existing;
    }

    private static function runtimeNamespaceFromVapiPath(string $path): string
    {
        $base = basename($path, '.vapi');
        if (!str_starts_with($base, 'phpef-runtime-native-')) {
            return 'PhpefRuntimeNative';
        }
        $suffix = substr($base, strlen('phpef-runtime-native-'));
        $parts = preg_split('/[^A-Za-z0-9]+/', $suffix) ?: [];
        return 'PhpefRuntimeNative' . implode('', array_map(
            static fn (string $part): string => $part === '' ? '' : ucfirst(strtolower($part)),
            $parts,
        ));
    }

    /**
     * Read runtime wrapper function names, min/max arity and signature types from a VAPI file.
     */
    private static function loadRuntimeFunctions(string $path, string $namespace, bool $required, array $metadataByWrapper = []): array
    {
        $vapi = @file_get_contents($path);
        if ($vapi === false) {
            if ($required) {
                throw new RuntimeException('Could not read phpef runtime VAPI for function resolution: ' . $path);
            }
            return [];
        }


        $functions = [];
        $pendingRuntimeMetadata = [];
        foreach (explode("\n", $vapi) as $line) {
            $trimmed = trim($line);
            if (str_starts_with($trimmed, '// PHPEF typed_fast_path ')) {
                $pendingRuntimeMetadata = self::parseRuntimeMetadataComment($trimmed);
                continue;
            }

            if (preg_match('/\bpublic\s+static\s+([A-Za-z0-9_?.<>\[\]]+)\s+([A-Za-z_][A-Za-z0-9_]*)(?:\s*<[^>]+>)?\s*\(([^)]*)\)/', $line, $match) !== 1) {
                if ($trimmed !== '' && !str_starts_with($trimmed, '//') && !str_starts_with($trimmed, '[')) {
                    $pendingRuntimeMetadata = [];
                }
                continue;
            }

            $returnType = $match[1];
            $functionName = $match[2];
            $argumentList = $match[3];
            $arity = self::vapiArgumentRange($argumentList);
            $functions[$functionName] = [
                'namespace' => $namespace,
                'minArgs' => $arity['minArgs'],
                'maxArgs' => $arity['maxArgs'],
                'returnType' => $returnType,
                'parameters' => self::vapiParameters($argumentList),
                'runtimeMetadata' => array_merge($pendingRuntimeMetadata, $metadataByWrapper[$functionName] ?? []),
            ];
            $pendingRuntimeMetadata = [];
        }

        return $functions;
    }

    /**
     * Read the machine-readable excluded-functions metadata block from a VAPI.
     *
     * Expected VAPI comment block:
     * // PHPEF_RUNTIME_EMBED_EXCLUDED_FUNCTIONS_BEGIN v1
     * // {"extension":"json","functions":["json_decode", ...]}
     * // PHPEF_RUNTIME_EMBED_EXCLUDED_FUNCTIONS_END
     */
    private static function loadExcludedRuntimeFunctions(string $path): array
    {
        $vapi = @file_get_contents($path);
        if ($vapi === false) {
            return [];
        }

        $jsonLines = [];
        $inBlock = false;
        foreach (explode("\n", $vapi) as $line) {
            if (str_contains($line, 'PHPEF_RUNTIME_EMBED_EXCLUDED_FUNCTIONS_BEGIN')) {
                $inBlock = true;
                continue;
            }
            if (str_contains($line, 'PHPEF_RUNTIME_EMBED_EXCLUDED_FUNCTIONS_END')) {
                $inBlock = false;
                continue;
            }
            if (!$inBlock) {
                continue;
            }

            $jsonLine = trim($line);
            if (str_starts_with($jsonLine, '//')) {
                $jsonLine = trim(substr($jsonLine, 2));
            }
            if ($jsonLine !== '') {
                $jsonLines[] = $jsonLine;
            }
        }

        if ($jsonLines === []) {
            return [];
        }

        $excluded = [];
        foreach (self::extractJsonObjectTexts(implode("\n", $jsonLines)) as $objectText) {
            self::addExcludedRuntimeObject($objectText, $excluded);
        }

        return $excluded;
    }

    /**
     * Read zendcall-only function metadata from VAPI comments emitted by
     * phpef-runtime-embed. These comments sit directly next to the generated
     * phpfunc_zendcall_* declarations and carry the reason why the normal
     * phpfunc_* wrapper is intentionally not exposed.
     */
    private static function loadZendcallOnlyRuntimeFunctions(string $path): array
    {
        $vapi = @file_get_contents($path);
        if ($vapi === false) {
            return [];
        }

        $functions = [];
        foreach (explode("\n", $vapi) as $line) {
            if (!preg_match(
                '#/\/ PHP ([^:]+): ([A-Za-z_][A-Za-z0-9_]*) \(as (zendcall_[A-Za-z_][A-Za-z0-9_]*)\); wrapper (phpfunc_zendcall_[A-Za-z_][A-Za-z0-9_]*)(?:; zendcall_reason: (.*))?$#',
                trim($line),
                $matches,
            )) {
                continue;
            }

            $functionName = strtolower($matches[2]);
            $functions[$functionName] = [
                'extension' => trim($matches[1]),
                'name' => $functionName,
                'callableName' => $matches[3],
                'wrapperName' => $matches[4],
                'reason' => trim($matches[5] ?? ''),
            ];
        }

        return $functions;
    }

    private function mergeZendcallOnlyRuntimeFunctions(array $functions): void
    {
        foreach ($functions as $functionName => $metadata) {
            $this->zendcallOnlyRuntimeFunctions[$functionName] = $metadata;
        }
    }

    private function mergeExcludedRuntimeFunctions(array $excluded): void
    {
        foreach ($excluded as $functionName => $extensions) {
            foreach ($extensions as $extension => $_) {
                $this->excludedRuntimeFunctions[$functionName][$extension] = true;
            }
        }
    }

    /**
     * Return normalized function => extension => true for one JSON object from the metadata block.
     */
    private static function addExcludedRuntimeObject(string $objectText, array &$excluded): void
    {
        if (!preg_match('/"extension"\s*:\s*"((?:\\\\.|[^"\\\\])*)"/s', $objectText, $extensionMatch)) {
            return;
        }
        if (!preg_match('/"functions"\s*:\s*\[(.*?)\]/s', $objectText, $functionsMatch)) {
            return;
        }

        $extension = self::jsonStringUnescape($extensionMatch[1]);
        if ($extension === '') {
            return;
        }

        if (preg_match_all('/"((?:\\\\.|[^"\\\\])*)"/', $functionsMatch[1], $functionMatches)) {
            foreach ($functionMatches[1] as $encodedFunction) {
                $functionName = strtolower(self::jsonStringUnescape($encodedFunction));
                if (preg_match('/^[a-z_][a-z0-9_]*$/', $functionName) !== 1) {
                    continue;
                }
                $excluded[$functionName][$extension] = true;
            }
        }
    }

    /**
     * Extract JSON object texts from the comment block without depending on json_decode.
     */
    private static function extractJsonObjectTexts(string $text): array
    {
        $objects = [];
        $length = strlen($text);
        $depth = 0;
        $start = null;
        $inString = false;
        $escaped = false;

        for ($i = 0; $i < $length; $i++) {
            $char = $text[$i];

            if ($inString) {
                if ($escaped) {
                    $escaped = false;
                    continue;
                }
                if ($char === '\\') {
                    $escaped = true;
                    continue;
                }
                if ($char === '"') {
                    $inString = false;
                }
                continue;
            }

            if ($char === '"') {
                $inString = true;
                continue;
            }
            if ($char === '{') {
                if ($depth === 0) {
                    $start = $i;
                }
                $depth++;
                continue;
            }
            if ($char === '}') {
                if ($depth > 0) {
                    $depth--;
                    if ($depth === 0 && $start !== null) {
                        $objects[] = substr($text, $start, $i - $start + 1);
                        $start = null;
                    }
                }
            }
        }

        return $objects;
    }

    private static function jsonStringUnescape(string $value): string
    {
        return str_replace(
            ['\\"', '\\\\', '\\/', '\\b', '\\f', '\\n', '\\r', '\\t'],
            ['"', '\\', '/', '', '', "\n", "\r", "\t"],
            $value,
        );
    }

    private function zendcallOnlyRuntimeFunctionForCall(string $phpCallName): ?array
    {
        $normalizedName = $this->normalizeCallName($phpCallName);
        return $this->zendcallOnlyRuntimeFunctions[$normalizedName] ?? null;
    }

    private function excludedRuntimeExtensionsForCall(string $phpCallName): array
    {
        $normalizedName = $this->normalizeCallName($phpCallName);
        if (!isset($this->excludedRuntimeFunctions[$normalizedName])) {
            return [];
        }

        $extensions = array_keys($this->excludedRuntimeFunctions[$normalizedName]);
        sort($extensions);
        return $extensions;
    }

    private static function formatExtensionList(array $extensions): string
    {
        $quoted = array_map(static fn (string $extension): string => '"' . $extension . '"', $extensions);
        if (count($quoted) === 1) {
            return 'extension ' . $quoted[0];
        }

        return 'extensions ' . implode(', ', $quoted);
    }


    private static function parseRuntimeMetadataComment(string $comment): array
    {
        if (preg_match('/^\/\/ PHPEF typed_fast_path ([A-Za-z0-9_]+)/', $comment, $match) !== 1) {
            return [];
        }

        $metadata = ['typed_fast_path' => $match[1]];
        foreach (['source', 'typed_signature', 'boundary'] as $key) {
            if (preg_match('/(?:^|; )' . preg_quote($key, '/') . ' ([^;]+)/', $comment, $keyMatch) === 1) {
                $metadata[$key] = trim($keyMatch[1]);
            }
        }
        if (preg_match('/(?:PHP function|PHP construct): ([^;]+)/', $comment, $phpMatch) === 1) {
            $metadata['php_name'] = strtolower(trim($phpMatch[1]));
        }

        return $metadata;
    }

    /**
     * Decide whether the typed fast path should receive the original typed arguments.
     *
     * @param array $candidate Runtime VAPI candidate descriptor.
     * @param array $runtimeMetadata Parsed PHPEF typed_fast_path metadata.
     */
    private static function typedFastPathPassesArguments(array $candidate, array $runtimeMetadata): bool
    {
        if (array_key_exists('pass_arguments', $runtimeMetadata)) {
            return (bool) $runtimeMetadata['pass_arguments'];
        }
        $parameters = $candidate['parameters'] ?? [];
        return is_array($parameters) && count($parameters) > 0;
    }

    private static function typedSignatureFromRuntimeMetadata(array $metadata): ?array
    {
        $returnType = $metadata['return_type'] ?? null;
        $argumentTypes = $metadata['argument_types'] ?? null;
        if (!is_string($returnType) && isset($metadata['typed_signature']) && is_string($metadata['typed_signature'])) {
            $parsed = self::parseTypedSignatureString($metadata['typed_signature']);
            if ($parsed !== null) {
                return $parsed;
            }
        }
        if (!is_string($returnType) || !is_array($argumentTypes)) {
            return null;
        }
        $normalizedArgs = [];
        foreach ($argumentTypes as $type) {
            if (!is_string($type)) {
                return null;
            }
            $normalizedArgs[] = self::normalizeTypedSignatureToken($type);
        }
        return [
            'returnType' => self::normalizeTypedSignatureToken($returnType),
            'argumentTypes' => $normalizedArgs,
        ];
    }

    private static function parseTypedSignatureString(string $signature): ?array
    {
        $signature = trim($signature);
        if (str_starts_with($signature, '_')) {
            $signature = substr($signature, 1);
        }
        if (!str_starts_with($signature, 'typed_')) {
            return null;
        }
        $fakeName = 'phpfunc_x_' . $signature;
        $parsed = self::parseTypedRuntimeName($fakeName);
        if ($parsed === null) {
            return null;
        }
        return [
            'returnType' => $parsed['returnType'],
            'argumentTypes' => $parsed['argumentTypes'],
        ];
    }

    private static function parseTypedRuntimeName(string $functionName): ?array
    {
        if (preg_match('/^phpfunc_(.+)_typed_([a-z0-9]+)((?:_arg[0-9]+_[a-z0-9_]+)*)$/', $functionName, $matches) !== 1) {
            return null;
        }

        $argumentTypes = [];
        $argumentSuffix = $matches[3];
        if ($argumentSuffix !== '') {
            $expectedIndex = 1;
            while ($argumentSuffix !== '') {
                if (preg_match('/^_arg([0-9]+)_([a-z0-9_]+?)(?=_arg[0-9]+_|$)/', $argumentSuffix, $argMatch) !== 1) {
                    return null;
                }
                if ((int) $argMatch[1] !== $expectedIndex) {
                    return null;
                }
                $argumentTypes[] = self::normalizeTypedSignatureToken($argMatch[2]);
                $argumentSuffix = substr($argumentSuffix, strlen($argMatch[0]));
                $expectedIndex++;
            }
        }

        return [
            'phpName' => strtolower($matches[1]),
            'returnType' => self::normalizeTypedSignatureToken($matches[2]),
            'argumentTypes' => $argumentTypes,
            'suffix' => substr($functionName, strlen('phpfunc_' . $matches[1] . '_')),
        ];
    }

    private static function typedSignatureSuffix(string $returnType, array $parameters): string
    {
        $parts = ['typed', self::typedSignatureTypeName($returnType)];
        $index = 1;
        foreach ($parameters as $parameter) {
            $parts[] = 'arg' . $index;
            $parts[] = self::typedSignatureTypeName($parameter['type'] ?? '');
            $index++;
        }

        return '_' . implode('_', $parts);
    }

    private static function typedSignatureTypeName(string $type): string
    {
        $type = strtolower(trim($type));
        $type = trim($type, '?');
        if ($type === 'phpefruntimecore.zval') {
            return 'zval';
        }
        if (str_ends_with($type, '.zval')) {
            return 'zval';
        }

        $type = str_replace('[]', '_array', $type);
        $type = preg_replace('/[^a-z0-9]+/', '_', $type);
        $type = trim($type, '_');
        return self::normalizeTypedSignatureToken($type === '' ? 'unknown' : $type);
    }

    private static function normalizeTypedSignatureToken(string $type): string
    {
        $token = strtolower(trim($type));
        if (str_contains($token, ':')) {
            [$base, $literal] = explode(':', $token, 2);
            return self::normalizeTypedSignatureToken($base) . ':' . $literal;
        }
        foreach (['bool' => ['true', 'false']] as $base => $literals) {
            foreach ($literals as $literal) {
                if ($token === $base . '_' . $literal) {
                    return self::normalizeTypedSignatureToken($base) . ':' . $literal;
                }
            }
        }
        return match ($token) {
            'int64', 'int32', 'integer', 'long', 'gint', 'gint64', 'gint32' => 'int',
            'float', 'double', 'gdouble', 'float32', 'float64' => 'double',
            'boolean', 'gboolean' => 'bool',
            'gchar', 'chararray', 'char_array', 'str' => 'string',
            default => $token,
        };
    }

    private static function vapiParameters(string $argumentList): array
    {
        $parameters = [];
        $args = array_values(array_filter(
            array_map(static fn (string $arg): string => trim($arg), explode(',', trim($argumentList))),
            static fn (string $arg): bool => $arg !== '',
        ));

        foreach ($args as $arg) {
            $declaration = $arg;
            $default = null;
            $equalsPosition = strpos($arg, '=');
            if ($equalsPosition !== false) {
                $declaration = trim(substr($arg, 0, $equalsPosition));
                $default = trim(substr($arg, $equalsPosition + 1));
            }

            $variadic = str_contains($declaration, '...') || preg_match('/^params\b/', $declaration) === 1;
            if (str_starts_with($declaration, 'params ')) {
                $declaration = substr($declaration, strlen('params '));
            }
            $declaration = trim(str_replace('...', '', $declaration));

            $type = $declaration;
            $name = '';
            if (preg_match('/^(.+)\s+([A-Za-z_][A-Za-z0-9_]*)$/', $declaration, $matches) === 1) {
                $type = trim($matches[1]);
                $name = $matches[2];
            }

            $parameters[] = [
                'name' => $name,
                'type' => $type,
                'optional' => $default !== null,
                'variadic' => $variadic,
                'default' => $default,
                'source' => $arg,
            ];
        }

        return $parameters;
    }

    /**
     * Return ['minArgs' => int, 'maxArgs' => ?int] for one VAPI argument list.
     *
     * Parameters with a default (`= ...`) are optional. Variadic parameters make
     * the upper bound unbounded. This intentionally stays VAPI-syntax based; it
     * does not need a separate signature registry.
     */
    private static function vapiArgumentRange(string $argumentList): array
    {
        $argumentList = trim($argumentList);
        if ($argumentList === '') {
            return ['minArgs' => 0, 'maxArgs' => 0];
        }

        $args = array_values(array_filter(
            array_map(static fn (string $arg): string => trim($arg), explode(',', $argumentList)),
            static fn (string $arg): bool => $arg !== '',
        ));

        $minArgs = 0;
        $maxArgs = 0;
        foreach ($args as $arg) {
            if (str_contains($arg, '...') || preg_match('/^params\b/', $arg) === 1) {
                return ['minArgs' => $minArgs, 'maxArgs' => null];
            }

            $maxArgs++;
            if (!str_contains($arg, '=')) {
                $minArgs++;
            }
        }

        return ['minArgs' => $minArgs, 'maxArgs' => $maxArgs];
    }

    private static function argumentCountMatches(int $argumentCount, int $minArgs, ?int $maxArgs): bool
    {
        if ($argumentCount < $minArgs) {
            return false;
        }
        if ($maxArgs !== null && $argumentCount > $maxArgs) {
            return false;
        }
        return true;
    }

    private static function formatArgumentRange(int $minArgs, ?int $maxArgs): string
    {
        if ($maxArgs === null) {
            return $minArgs . '..*';
        }
        if ($minArgs === $maxArgs) {
            return (string) $minArgs;
        }
        return $minArgs . '..' . $maxArgs;
    }

    /**
     * Validate and normalize a PHP/ValaIR call name.
     */
    private function normalizeCallName(string $name): string
    {
        if (!preg_match('/^[A-Za-z_][A-Za-z0-9_]*$/', $name)) {
            throw new RuntimeException('Invalid PHP call name for runtime resolution: ' . $name);
        }

        return strtolower($name);
    }
}
