<?php

/**
 * Generates tools/phpstan/constants.stub.php by scanning every define() call in
 * src/ and inferring a rough type for each constant. Run from the project root:
 *
 *   php tools/phpstan/gen-constants-stub.php > tools/phpstan/constants.stub.php
 *
 * @package XC_VM
 */

$root = dirname(__DIR__, 2) . '/src';
$dir = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($root, FilesystemIterator::SKIP_DOTS));
$consts = [];

foreach ($dir as $f) {
    if ($f->getExtension() !== 'php') {
        continue;
    }
    $src = @file_get_contents($f->getPathname());
    if ($src === false || strpos($src, 'define(') === false) {
        continue;
    }
    $tokens = token_get_all($src);
    $n = count($tokens);
    for ($i = 0; $i < $n; $i++) {
        $t = $tokens[$i];
        if (!(is_array($t) && $t[0] === T_STRING && $t[1] === 'define')) {
            continue;
        }
        $name = null;
        $k = $i + 1;
        for (; $k < $n && $k < $i + 40; $k++) {
            $tk = $tokens[$k];
            if (is_array($tk) && $tk[0] === T_CONSTANT_ENCAPSED_STRING) {
                $name = trim($tk[1], "'\"");
                break;
            }
            if ($tk === ')') {
                break;
            }
        }
        if (!$name || !preg_match('/^[A-Z_][A-Z0-9_]*$/', $name) || isset($consts[$name])) {
            continue;
        }
        $hasIntLit = $hasStrLit = $hasConcat = $hasBool = false;
        $isIntOnly = true;
        $seen = false;
        $d = 0;
        for ($m = $k + 1; $m < $n && $m < $k + 80; $m++) {
            $tm = $tokens[$m];
            if ($tm === '(') { $d++; continue; }
            if ($tm === ')') { if ($d === 0) break; $d--; continue; }
            if ($tm === ',' && $d === 0) { $seen = true; continue; }
            if (!$seen) { continue; }
            if (is_array($tm)) {
                if ($tm[0] === T_LNUMBER || $tm[0] === T_DNUMBER) { $hasIntLit = true; }
                elseif ($tm[0] === T_CONSTANT_ENCAPSED_STRING) { $hasStrLit = true; $isIntOnly = false; }
                elseif ($tm[0] === T_STRING && in_array(strtolower($tm[1]), ['true', 'false'], true)) { $hasBool = true; $isIntOnly = false; }
                elseif ($tm[0] === T_WHITESPACE) { /* skip */ }
                else { $isIntOnly = false; }
            } else {
                if ($tm === '.') { $hasConcat = true; $isIntOnly = false; }
                else { $isIntOnly = false; }
            }
        }
        if ($hasConcat || $hasStrLit) { $type = 'string'; }
        elseif ($hasBool && !$hasIntLit) { $type = 'bool'; }
        elseif ($hasIntLit && $isIntOnly) { $type = 'int'; }
        else { $type = 'mixed'; }
        $consts[$name] = $type;
    }
}

// Explicit type overrides for constants whose value the heuristic cannot
// classify (defined from variables or function calls). Keeps the stub accurate
// and prevents false null/mixed positives at call sites.
$overrides = [
    'HOST'       => 'string', // trim(explode(':', HTTP_HOST)[0])
    'PAGE_NAME'  => 'string',
    'PHP_ERRORS' => 'bool',   // dev-mode flag
];
foreach ($overrides as $name => $type) {
    if (isset($consts[$name])) {
        $consts[$name] = $type;
    }
}

ksort($consts);

echo "<?php\n\n";
echo "/**\n";
echo " * PHPStan constants stub for XC_VM (auto-generated by gen-constants-stub.php).\n";
echo " *\n";
echo " * The application defines these constants at RUNTIME inside methods, where\n";
echo " * PHPStan cannot see them via static scanning. Values are a cast of an unset\n";
echo " * env var so PHPStan gets the correct GENERAL type without narrowing to a\n";
echo " * literal (which would cause false always-true/false comparison reports).\n";
echo " *\n";
echo " * @package XC_VM\n";
echo " */\n\n";

foreach ($consts as $name => $type) {
    // Use mt_rand()-based expressions so PHPStan infers a GENERAL type
    // (int/string/bool) and never constant-folds to a literal (0/'') — a
    // literal would create false positives like division-by-zero or
    // always-false comparisons at call sites.
    $expr = match ($type) {
        'string' => "(string) mt_rand()",
        'int'    => "mt_rand()",
        'bool'   => "(bool) mt_rand()",
        default  => "json_decode((string) mt_rand(), true)",
    };
    echo "if (!defined('$name')) define('$name', $expr);\n";
}
