<?php
/** Conservative executable-token inventory, NOT runtime or compatibility evidence. PHP >= 7.3. */
function inventoryTokens($source) {
    $out = []; $offset = 0; $line = 1; $column = 1; $quoted = false; $heredoc = false;
    foreach (token_get_all($source) as $token) {
        $id = is_array($token) ? $token[0] : null;
        $text = is_array($token) ? $token[1] : $token;
        $startLine = $line; $startColumn = $column;
        $newlines = substr_count($text, "\n");
        $line += $newlines;
        $column = $newlines ? strlen($text) - strrpos($text, "\n") : $column + strlen($text);
        $offset += strlen($text);
        if ($id === T_START_HEREDOC) { $heredoc = true; continue; }
        if ($heredoc) { if ($id === T_END_HEREDOC) { $heredoc = false; } continue; }
        if ($id === null && ($text === '"' || $text === '`')) { $quoted = !$quoted; continue; }
        if ($quoted || in_array($id, [T_WHITESPACE, T_COMMENT, T_DOC_COMMENT, T_OPEN_TAG, T_CLOSE_TAG, T_INLINE_HTML], true)) { continue; }
        $out[] = ['id' => $id, 'text' => $text, 'line' => $startLine, 'column' => $startColumn];
    }
    return $out;
}

function inventoryNameToken($token) {
    return $token['id'] === T_STRING || $token['id'] === T_NS_SEPARATOR ||
        (defined('T_NAME_QUALIFIED') && in_array($token['id'], [T_NAME_QUALIFIED, T_NAME_FULLY_QUALIFIED, T_NAME_RELATIVE], true));
}

function inventoryReadName($tokens, $start, &$end) {
    $name = ''; $end = $start;
    while (isset($tokens[$end]) && inventoryNameToken($tokens[$end])) { $name .= $tokens[$end++]['text']; }
    return $name;
}

function inventoryResolveName($name, $namespace, $aliases) {
    if (substr($name, 0, 1) === '\\') { return ltrim($name, '\\'); }
    $parts = explode('\\', $name); $first = strtolower($parts[0]);
    if (isset($aliases[$first])) { array_shift($parts); return $aliases[$first] . ($parts ? '\\' . implode('\\', $parts) : ''); }
    if (strpos(strtolower($name), 'namespace\\') === 0) { return trim($namespace . '\\' . substr($name, 10), '\\'); }
    return trim($namespace . '\\' . $name, '\\');
}

function inventoryUseAliases($tokens, $start, &$end) {
    $out = []; $prefix = ''; $name = ''; $alias = ''; $as = false;
    $end = $start;
    for (; isset($tokens[$end]); $end++) {
        $t = $tokens[$end]; $text = $t['text'];
        if ($text === ';' || $text === ',' || $text === '}') {
            if ($name !== '') { $fqn = trim($prefix . $name, '\\'); $parts = explode('\\', $fqn); $out[strtolower($alias ?: end($parts))] = $fqn; }
            $name = ''; $alias = ''; $as = false;
            if ($text === ';') { break; }
        } elseif ($text === '{') { $prefix = $name; $name = ''; }
        elseif ($t['id'] === T_AS) { $as = true; }
        elseif (inventoryNameToken($t)) { if ($as) { $alias .= $text; } else { $name .= $text; } }
    }
    return $out;
}

/** Render receiver without argument literals, and find its lexical root. */
function inventoryReceiver($tokens, $operatorIndex) {
    $parts = []; $depth = 0;
    for ($j = $operatorIndex - 1; $j >= 0; $j--) {
        $t = $tokens[$j]; $text = $t['text'];
        if ($text === ')' || $text === ']') {
            $close = $text; $open = $close === ')' ? '(' : '['; $level = 1;
            while (--$j >= 0) { if ($tokens[$j]['text'] === $close) { $level++; } elseif ($tokens[$j]['text'] === $open && --$level === 0) { break; } }
            array_unshift($parts, $close === ')' ? '(...)' : '[...]');
            continue;
        }
        if (inventoryNameToken($t) || $t['id'] === T_VARIABLE || $t['id'] === T_STATIC || in_array($text, ['->', '?->', '::'], true)) {
            array_unshift($parts, $text); continue;
        }
        break;
    }
    return implode('', $parts);
}

function inventorySource($source, $repo, $file) {
    $tokens = inventoryTokens($source); $rows = []; $namespace = ''; $aliases = [];
    $depth = 0; $classes = []; $functions = []; $opens = [];
    $terminal = array_fill_keys(explode(' ', 'select find findorfail get value column count sum avg min max insert insertall insertgetid insertorignore update delete destroy save saveall setinc setdec increment decrement query execute exec prepare fetch fetchall fetchcolumn paginate chunk cursor lazy starttrans begintransaction transaction commit rollback getlastinsid lastinsertid getfields gettableinfo gettables procedure'), true);
    $relations = array_fill_keys(explode(' ', 'hasone hasmany belongsto belongstomany morphone morphmany morphto'), true);
    for ($i = 0, $n = count($tokens); $i < $n; $i++) {
        $t = $tokens[$i]; $text = $t['text']; $id = $t['id'];
        if ($id === T_NAMESPACE && isset($tokens[$i + 1]) && $tokens[$i + 1]['text'] !== '\\') { $namespace = inventoryReadName($tokens, $i + 1, $end); $aliases = []; }
        if ($id === T_USE && !$classes && !$functions && isset($tokens[$i + 1]) && $tokens[$i + 1]['text'] !== '(' && !in_array($tokens[$i + 1]['id'], [T_FUNCTION, T_CONST], true)) { $aliases = array_merge($aliases, inventoryUseAliases($tokens, $i + 1, $end)); }
        if (in_array($id, [T_CLASS, T_INTERFACE, T_TRAIT], true) && (!isset($tokens[$i - 1]) || $tokens[$i - 1]['text'] !== '::')) {
            $name = isset($tokens[$i + 1]) && $tokens[$i + 1]['id'] === T_STRING ? $tokens[$i + 1]['text'] : '{anonymous@' . $t['line'] . '}';
            for ($j = $i + 1; $j < $n && !in_array($tokens[$j]['text'], ['{', ';'], true); $j++) {}
            if ($j < $n && $tokens[$j]['text'] === '{') { $opens[$j]['class'] = trim($namespace . '\\' . $name, '\\'); }
        }
        if ($id === T_FUNCTION) {
            $j = $i + 1; if (isset($tokens[$j]) && $tokens[$j]['text'] === '&') { $j++; }
            $name = isset($tokens[$j]) && $tokens[$j]['id'] === T_STRING ? $tokens[$j]['text'] : '{closure@' . $t['line'] . '}';
            $level = 0;
            for (; $j < $n; $j++) {
                if ($tokens[$j]['text'] === '(') { $level++; }
                elseif ($tokens[$j]['text'] === ')') { $level--; }
                elseif ($level === 0 && in_array($tokens[$j]['text'], ['{', ';'], true)) { break; }
            }
            if ($j < $n && $tokens[$j]['text'] === '{') { $opens[$j]['function'] = $name; }
        }
        if ($text === '{') {
            $depth++;
            if (isset($opens[$i]['class'])) { $classes[] = ['name' => $opens[$i]['class'], 'depth' => $depth]; }
            if (isset($opens[$i]['function'])) { $functions[] = ['name' => $opens[$i]['function'], 'depth' => $depth]; }
        } elseif ($text === '}') {
            while ($classes && end($classes)['depth'] === $depth) { array_pop($classes); }
            while ($functions && end($functions)['depth'] === $depth) { array_pop($functions); }
            $depth--;
        }
        $class = $classes ? end($classes)['name'] : '';
        $function = $functions ? end($functions)['name'] : '';
        $previous = isset($tokens[$i - 1]) ? $tokens[$i - 1]['text'] : '';
        $next = isset($tokens[$i + 1]) ? $tokens[$i + 1]['text'] : '';
        $kind = ''; $receiver = ''; $method = strtolower($text); $reason = '';
        if (in_array($previous, ['->', '?->', '::'], true) && ($id === T_VARIABLE || $text === '{')) {
            $end = $i;
            if ($text === '{') { $level = 1; while (++$end < $n) { if ($tokens[$end]['text'] === '{') { $level++; } elseif ($tokens[$end]['text'] === '}' && --$level === 0) { break; } } }
            if (isset($tokens[$end + 1]) && $tokens[$end + 1]['text'] === '(') { $kind = 'dynamic_call_unresolved'; $receiver = inventoryReceiver($tokens, $i - 1); $method = '{dynamic}'; $reason = 'Dynamic method syntax; cannot resolve statically'; }
        } elseif ($id === T_STRING && $next === '(' && in_array($previous, ['->', '?->', '::'], true)) {
            $receiver = inventoryReceiver($tokens, $i - 1);
            preg_match('/^([^(:$]+)(?:::|$)/', $receiver, $rootMatch);
            $root = isset($rootMatch[1]) ? $rootMatch[1] : '';
            $resolved = inventoryResolveName($root, $namespace, $aliases);
            $facade = in_array(strtolower($resolved), ['think\\db', 'think\\facade\\db'], true) || (strtolower($root) === 'db' && !isset($aliases['db']));
            $model = strpos(strtolower($resolved), '\\model\\') !== false || preg_match('/model$/i', $root) || (in_array(strtolower($root), ['self', 'static', 'parent'], true) && strpos(strtolower($class), '\\model\\') !== false);
            if ($facade) { $kind = $previous === '::' ? 'facade_entry' : (isset($terminal[$method]) ? 'facade_chain_terminal' : 'facade_chain_builder'); $reason = 'ThinkPHP Db facade lexical chain'; }
            elseif ($model) { $kind = isset($terminal[$method]) ? 'model_chain_terminal' : 'model_chain_builder'; $reason = 'Imported model namespace/name or model-class self/static heuristic'; }
            elseif (isset($terminal[$method]) || isset($relations[$method])) { $kind = $previous === '::' ? 'static_terminal_candidate' : 'object_terminal_candidate'; $reason = 'Method name matches ORM/PDO; receiver type unresolved and may be non-DB'; }
        } elseif ($id === T_STRING && $next === '(' && !in_array($previous, ['->', '?->', '::'], true) && (!isset($tokens[$i - 1]) || $tokens[$i - 1]['id'] !== T_FUNCTION)) {
            if ($method === 'db') { $kind = 'db_helper_candidate'; $reason = 'db() helper name; resolution requires runtime'; }
            elseif (preg_match('/^(?:mysql|mysqli|pg|oci|dm)_(?:query|execute|exec|prepare|fetch.*|connect|pconnect|commit|rollback)$/', $method)) { $kind = 'raw_driver_function'; $reason = 'Database driver function name'; }
        }
        if ($id === T_NEW) {
            $name = inventoryReadName($tokens, $i + 1, $end);
            if (in_array(strtolower(inventoryResolveName($name, $namespace, $aliases)), ['pdo', 'mysqli'], true)) { $kind = 'raw_connection_constructor'; $receiver = ltrim($name, '\\'); $method = '__construct'; $reason = 'Raw database connection constructor'; }
        }
        if ($kind !== '') {
            $phase = isset($terminal[$method]) ? 'execution_candidate' : 'builder_candidate';
            $operation = 'unknown';
            if (in_array($method, explode(' ', 'select find findorfail get value column count sum avg min max fetch fetchall fetchcolumn paginate chunk cursor lazy'), true)) { $operation = 'read'; }
            elseif (in_array($method, explode(' ', 'insert insertall insertgetid insertorignore update delete destroy save saveall setinc setdec increment decrement'), true)) { $operation = 'write'; }
            elseif (in_array($method, explode(' ', 'starttrans begintransaction transaction commit rollback'), true)) { $operation = 'transaction'; }
            elseif (in_array($method, explode(' ', 'getlastinsid lastinsertid getfields gettableinfo gettables'), true)) { $operation = 'metadata'; }
            elseif (in_array($method, ['query', 'execute', 'exec', 'procedure'], true)) { $operation = 'raw_sql_unknown'; }
            if ($kind === 'dynamic_call_unresolved') { $phase = 'dynamic_unresolved'; }
            elseif ($kind === 'raw_connection_constructor') { $phase = 'connection_candidate'; $operation = 'connection'; }
            $rows[] = ['callsite_id' => hash('sha256', $repo . '|' . $file . '|' . $t['line'] . '|' . $t['column'] . '|' . $method), 'repo' => $repo, 'relative_file' => $file, 'line' => $t['line'], 'column' => $t['column'], 'class' => $class, 'function' => $function, 'call_kind' => $kind, 'phase' => $phase, 'operation' => $operation, 'receiver' => $receiver, 'method' => $method, 'status' => 'potential', 'reason' => $reason, 'source_sha256' => hash('sha256', $source)];
        }
    }
    return $rows;
}

function inventoryMain($argv) {
    if (count($argv) !== 3) { fwrite(STDERR, "Usage: php inventory.php <zhct workspace root> <output prefix>\n"); return 2; }
    $workspace = realpath($argv[1]); $prefix = $argv[2]; $rows = []; $files = []; $errors = [];
    if ($workspace === false) { throw new RuntimeException('Workspace does not exist'); }
    foreach (['store' => 'application', 'ai_api' => 'app'] as $repo => $scanDir) {
        $repoRoot = $workspace . '/zhctproject/' . $repo; $root = $repoRoot . '/' . $scanDir;
        if (!is_dir($root) || is_link($root) || is_link($repoRoot)) { throw new RuntimeException('Missing directory or unverified symlink: ' . $root); }
        $paths = [];
        foreach (new RecursiveIteratorIterator(new RecursiveDirectoryIterator($root, FilesystemIterator::SKIP_DOTS)) as $entry) { if ($entry->isFile() && !$entry->isLink() && strtolower($entry->getExtension()) === 'php') { $paths[] = $entry->getPathname(); } }
        sort($paths);
        foreach ($paths as $path) {
            $relative = substr($path, strlen($repoRoot) + 1); $source = file_get_contents($path);
            if ($source === false) { $errors[] = ['repo' => $repo, 'relative_file' => $relative, 'error' => 'unreadable']; continue; }
            $calls = inventorySource($source, $repo, $relative); $rows = array_merge($rows, $calls);
            $files[] = ['repo' => $repo, 'relative_file' => $relative, 'source_sha256' => hash('sha256', $source), 'candidates' => count($calls)];
        }
    }
    $handle = fopen($prefix . '.csv', 'w');
    if ($handle === false) { throw new RuntimeException('Cannot open CSV output'); }
    if ($rows) { fputcsv($handle, array_keys($rows[0])); foreach ($rows as $row) { fputcsv($handle, $row); } }
    fclose($handle);
    $counts = []; foreach ($rows as $row) { $key = $row['repo']; if (!isset($counts[$key])) { $counts[$key] = ['candidates' => 0, 'by_call_kind' => [], 'by_phase' => [], 'by_operation' => []]; } $counts[$key]['candidates']++; foreach (['call_kind', 'phase', 'operation'] as $field) { $value = $row[$field]; $counts[$key]['by_' . $field][$value] = isset($counts[$key]['by_' . $field][$value]) ? $counts[$key]['by_' . $field][$value] + 1 : 1; } }
    $summary = ['schema_version' => 1, 'generated_at_utc' => gmdate('c'), 'status' => 'potential_only_not_test_evidence', 'scan_roots' => ['store/application', 'ai_api/app'], 'php_version' => PHP_VERSION, 'file_count' => count($files), 'candidate_count' => count($rows), 'counts' => $counts, 'errors' => $errors, 'limitations' => ['No autoload, class hierarchy resolution or program execution', 'Unknown object terminal methods can be non-DB (e.g. spreadsheet save); manual/runtime adjudication required', 'Facade/model builders and connection constructors do not prove SQL execution', 'Dynamic calls retained but target unresolved; magic methods, wrappers and dependencies outside scan roots may be absent', 'Static file/line matches are source-version specific and do not establish branch, data-state or transaction coverage', 'No raw SQL literals, bindings, DSNs or credentials emitted']];
    file_put_contents($prefix . '.summary.json', json_encode($summary, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "\n");
    file_put_contents($prefix . '.files.json', json_encode($files, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "\n");
    echo json_encode($summary, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "\n";
    return $errors ? 1 : 0;
}

if (isset($_SERVER['SCRIPT_FILENAME']) && realpath($_SERVER['SCRIPT_FILENAME']) === __FILE__) { exit(inventoryMain($argv)); }
