<?php
/**
 * Site-framework detection + targeted diagnostic context.
 *
 * Used by scripts/process-log-review.php to stop assuming every site is
 * WordPress. The detector inspects real files on disk under
 * /docker/users/{username}/{safe_domain}/ and picks a framework label
 * with high confidence (no guessing from log lines).
 */

if (!function_exists('whp_detect_framework')) {

    function whp_detect_framework(string $username, string $safeDomain): array {
        $base = "/docker/users/{$username}/{$safeDomain}";
        $public = $base . '/public_html';
        $app    = $base . '/app';

        // 1. WordPress — wp-config.php or wp-includes/version.php
        if (is_file($public . '/wp-config.php') || is_file($public . '/wp-includes/version.php')) {
            $version = null;
            $verFile = $public . '/wp-includes/version.php';
            if (is_readable($verFile)) {
                $content = (string)@file_get_contents($verFile);
                if (preg_match('/\$wp_version\s*=\s*[\'"]([^\'"]+)[\'"]/', $content, $m)) {
                    $version = $m[1];
                }
            }
            return [
                'framework'  => 'wordpress',
                'version'    => $version,
                'indicators' => ['public_html/wp-config.php'],
                'site_root'  => $public,
                'layout'     => 'public_html',
            ];
        }

        // 2. Laravel — artisan + composer.json mentioning laravel/framework
        foreach ([$public, $base] as $dir) {
            if (is_file($dir . '/artisan')) {
                $version = whp_fw_composer_dep_version($dir . '/composer.json', 'laravel/framework');
                return [
                    'framework'  => 'laravel',
                    'version'    => $version,
                    'indicators' => [($dir === $public ? 'public_html/' : '') . 'artisan'],
                    'site_root'  => $dir,
                    'layout'     => ($dir === $public) ? 'public_html' : 'other',
                ];
            }
        }

        // 3. Node-based layouts — package.json under app/, base, or public_html
        foreach ([$app, $base, $public] as $dir) {
            $pkgPath = $dir . '/package.json';
            if (!is_file($pkgPath)) continue;
            $pkg = whp_fw_parse_json($pkgPath);
            if ($pkg === null) continue;
            $deps = array_merge(
                $pkg['dependencies']        ?? [],
                $pkg['devDependencies']     ?? [],
                $pkg['peerDependencies']    ?? []
            );
            $depNames = array_keys(is_array($deps) ? $deps : []);
            $layout = ($dir === $app) ? 'app' : (($dir === $public) ? 'public_html' : 'other');

            if (in_array('next', $depNames, true)) {
                return ['framework' => 'nextjs', 'version' => $deps['next'] ?? null,
                        'indicators' => ['package.json:next'], 'site_root' => $dir, 'layout' => $layout];
            }
            if (in_array('nuxt', $depNames, true) || in_array('nuxt3', $depNames, true)) {
                return ['framework' => 'nuxt', 'version' => $deps['nuxt'] ?? ($deps['nuxt3'] ?? null),
                        'indicators' => ['package.json:nuxt'], 'site_root' => $dir, 'layout' => $layout];
            }
            $primary = null;
            foreach (['express', 'fastify', 'koa', 'hapi', '@nestjs/core'] as $cand) {
                if (in_array($cand, $depNames, true)) { $primary = $cand; break; }
            }
            return ['framework' => 'node', 'version' => $primary ? ($deps[$primary] ?? null) : ($pkg['version'] ?? null),
                    'indicators' => array_values(array_filter(['package.json', $primary])),
                    'site_root' => $dir, 'layout' => $layout];
        }

        // 4. Django — manage.py
        foreach ([$app, $base, $public] as $dir) {
            if (is_file($dir . '/manage.py')) {
                return ['framework' => 'django', 'version' => null,
                        'indicators' => ['manage.py'], 'site_root' => $dir,
                        'layout' => ($dir === $app) ? 'app' : 'other'];
            }
        }

        // 5. Rails — Gemfile with rails
        foreach ([$app, $base, $public] as $dir) {
            $gf = $dir . '/Gemfile';
            if (!is_file($gf)) continue;
            $g = (string)@file_get_contents($gf);
            if (preg_match("/^\\s*gem\\s+['\"]rails['\"]/m", $g)) {
                return ['framework' => 'rails', 'version' => null,
                        'indicators' => ['Gemfile:rails'], 'site_root' => $dir,
                        'layout' => ($dir === $app) ? 'app' : 'other'];
            }
        }

        // 6. Plain PHP / static
        if (is_dir($public)) {
            $files = @scandir($public) ?: [];
            foreach ($files as $f) {
                if (substr($f, -4) === '.php') {
                    return ['framework' => 'plain_php', 'version' => null,
                            'indicators' => ['public_html/' . $f], 'site_root' => $public,
                            'layout' => 'public_html'];
                }
            }
            if (!empty($files)) {
                return ['framework' => 'static', 'version' => null,
                        'indicators' => ['public_html/'], 'site_root' => $public,
                        'layout' => 'public_html'];
            }
        }

        return ['framework' => 'unknown', 'version' => null,
                'indicators' => [], 'site_root' => $base, 'layout' => 'other'];
    }

    function whp_fw_parse_json(string $path): ?array {
        $raw = @file_get_contents($path);
        if ($raw === false) return null;
        $json = json_decode($raw, true);
        return is_array($json) ? $json : null;
    }

    function whp_fw_composer_dep_version(string $composerPath, string $pkg): ?string {
        $c = whp_fw_parse_json($composerPath);
        if (!$c) return null;
        $v = $c['require'][$pkg] ?? ($c['require-dev'][$pkg] ?? null);
        return $v ?: null;
    }

    /**
     * Build a sanitized diagnostic context block the AI prompt can include.
     */
    function whp_framework_context(array $detect, array $containers = []): string {
        $fw = $detect['framework'] ?? 'unknown';
        $root = $detect['site_root'] ?? '';
        $parts = [];

        $parts[] = "=== Site stack ===";
        $parts[] = "Framework: {$fw}" . (!empty($detect['version']) ? " (version: {$detect['version']})" : '');
        if (!empty($detect['indicators'])) {
            $parts[] = "Indicators: " . implode(', ', $detect['indicators']);
        }
        $parts[] = "Site root: {$root}";
        $parts[] = "";

        switch ($fw) {
            case 'wordpress': $parts[] = whp_fw_wp_context($root);                 break;
            case 'laravel':   $parts[] = whp_fw_laravel_context($root);            break;
            case 'nextjs':
            case 'nuxt':
            case 'node':      $parts[] = whp_fw_node_context($root, $containers);  break;
            case 'django':    $parts[] = whp_fw_python_context($root);             break;
            case 'rails':     $parts[] = whp_fw_rails_context($root);              break;
            case 'plain_php':
            case 'static':
            case 'unknown':
            default:          $parts[] = whp_fw_generic_context($root);            break;
        }
        return implode("\n", array_filter($parts));
    }

    function whp_fw_du(string $path): string {
        return @trim((string)shell_exec("du -sh " . escapeshellarg($path) . " 2>/dev/null | cut -f1"));
    }

    function whp_fw_count(string $path): string {
        return @trim((string)shell_exec("find " . escapeshellarg($path) . " -mindepth 1 -maxdepth 1 2>/dev/null | wc -l"));
    }

    function whp_fw_wp_context(string $root): string {
        $out = ["=== WordPress context ==="];

        $cfg = $root . '/wp-config.php';
        if (is_readable($cfg)) {
            $raw = (string)@file_get_contents($cfg);
            $wanted = [
                'WP_DEBUG', 'WP_DEBUG_LOG', 'WP_DEBUG_DISPLAY',
                'WP_CACHE', 'DISABLE_WP_CRON', 'WP_POST_REVISIONS',
                'WP_MEMORY_LIMIT', 'WP_MAX_MEMORY_LIMIT',
                'WP_AUTO_UPDATE_CORE', 'AUTOMATIC_UPDATER_DISABLED',
                'FS_METHOD', 'FORCE_SSL_ADMIN',
            ];
            $found = [];
            foreach ($wanted as $k) {
                if (preg_match('/define\s*\(\s*[\'"]' . preg_quote($k, '/') . '[\'"]\s*,\s*([^;]+?)\s*\)\s*;/i', $raw, $m)) {
                    $found[] = "{$k} = " . trim($m[1]);
                }
            }
            $out[] = "wp-config defines (non-secret):";
            $out[] = $found ? "  " . implode("\n  ", $found) : "  (none of the common tuning constants are set)";
            if (preg_match('/\$table_prefix\s*=\s*[\'"]([^\'"]+)[\'"]/', $raw, $m)) {
                $out[] = "  table_prefix = {$m[1]}";
            }
        } else {
            $out[] = "wp-config.php not readable from the review worker.";
        }

        $out[] = "";
        $out[] = "Directory sizes:";
        foreach ([
            'wp-content/cache', 'wp-content/uploads', 'wp-content/mu-plugins',
            'wp-content/plugins', 'wp-content/themes',
        ] as $sub) {
            $p = $root . '/' . $sub;
            if (is_dir($p)) {
                $out[] = "  {$sub}: " . whp_fw_du($p) . " (" . whp_fw_count($p) . " entries)";
            }
        }

        $ht = $root . '/.htaccess';
        if (is_file($ht)) {
            $lines = @count(@file($ht, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) ?: []);
            $out[] = "";
            $out[] = ".htaccess: {$lines} non-empty lines";
        }

        $mu = $root . '/wp-content/mu-plugins';
        if (is_dir($mu)) {
            $muFiles = @glob($mu . '/*.php') ?: [];
            if (!empty($muFiles)) {
                $names = array_map('basename', $muFiles);
                $out[] = "mu-plugins: " . implode(', ', array_slice($names, 0, 20));
            }
        }
        return implode("\n", $out);
    }

    function whp_fw_laravel_context(string $root): string {
        $out = ["=== Laravel context ==="];
        $composer = whp_fw_parse_json($root . '/composer.json') ?: [];
        $require = $composer['require'] ?? [];
        if ($require) {
            $top = array_slice($require, 0, 15, true);
            foreach ($top as $k => $v) { $out[] = "  {$k}: {$v}"; }
        }
        $logs = $root . '/storage/logs';
        if (is_dir($logs)) {
            $files = @scandir($logs) ?: [];
            $rel = array_values(array_filter($files, fn($f) => $f[0] !== '.'));
            $out[] = "storage/logs entries: " . (count($rel) ? implode(', ', array_slice($rel, 0, 10)) : '(empty)');
        }
        $out[] = ".env: " . (is_file($root . '/.env') ? "present" : "missing");
        return implode("\n", $out);
    }

    function whp_fw_node_context(string $root, array $containers = []): string {
        $out = ["=== Node.js context ==="];
        $pkg = whp_fw_parse_json($root . '/package.json') ?: [];
        if (!empty($pkg['name']))        $out[] = "package: {$pkg['name']}" . (!empty($pkg['version']) ? "@{$pkg['version']}" : '');
        if (!empty($pkg['main']))        $out[] = "main: {$pkg['main']}";
        if (!empty($pkg['scripts']['start'])) $out[] = "start script: {$pkg['scripts']['start']}";
        if (!empty($pkg['engines']['node'])) $out[] = "required node: {$pkg['engines']['node']}";
        $deps = array_merge($pkg['dependencies'] ?? [], $pkg['devDependencies'] ?? []);
        if ($deps) {
            $top = array_slice($deps, 0, 15, true);
            $out[] = "top dependencies:";
            foreach ($top as $k => $v) { $out[] = "  {$k}: {$v}"; }
        }
        $out[] = "node_modules: " . (is_dir($root . '/node_modules') ? "present" : "MISSING (npm install has not been run)");
        if (is_file($root . '/ecosystem.config.js')) $out[] = "pm2 ecosystem.config.js: present";

        // PM2 process state (per container). WHP runs Node sites under
        // PM2; restart_time + unstable_restarts are the canonical signals
        // for a crash-looping app.
        if (!empty($containers)) {
            $pm2Lines = whp_fw_pm2_state_block($containers);
            if ($pm2Lines !== '') {
                $out[] = "";
                $out[] = $pm2Lines;
            }
        }
        return implode("\n", $out);
    }

    /**
     * Probe `pm2 jlist` inside each container and return a summarized block.
     * Containers with no PM2 / no apps are skipped silently. Returns '' if
     * no container produced PM2 output (so the caller can decide whether
     * to render the section header at all).
     */
    function whp_fw_pm2_state_block(array $containers): string {
        $blocks = [];
        foreach ($containers as $c) {
            if (!preg_match('/^[a-zA-Z0-9._-]+$/', $c)) continue;
            $safe = escapeshellarg($c);
            $jlist = trim((string)@shell_exec(
                "docker exec {$safe} sh -c 'command -v pm2 >/dev/null 2>&1 && pm2 jlist 2>/dev/null'"
            ));
            if ($jlist === '' || $jlist === 'null' || $jlist === '[]') continue;

            $procs = json_decode($jlist, true);
            if (!is_array($procs)) continue;

            $lines = ["  {$c}: " . count($procs) . " PM2 process(es)"];
            foreach ($procs as $p) {
                $name     = $p['name'] ?? '?';
                $env      = $p['pm2_env'] ?? [];
                $status   = $env['status'] ?? '?';
                $restarts = (int)($env['restart_time'] ?? 0);
                $unstable = (int)($env['unstable_restarts'] ?? 0);
                $uptimeMs = (int)($env['pm_uptime'] ?? 0);
                $uptimeS  = $uptimeMs > 0 ? max(0, time() - intdiv($uptimeMs, 1000)) : 0;
                $monit    = $p['monit'] ?? [];
                $memMB    = isset($monit['memory']) ? round($monit['memory'] / 1048576, 1) : 0;
                $cpu      = $monit['cpu'] ?? 0;
                $lines[]  = sprintf(
                    "    - %s: %s, restarts=%d, unstable=%d, uptime=%ds, mem=%sMB, cpu=%s%%",
                    $name, $status, $restarts, $unstable, $uptimeS, $memMB, $cpu
                );
            }
            $blocks[] = implode("\n", $lines);
        }
        return $blocks
            ? "PM2 process state (per container):\n" . implode("\n", $blocks)
            : '';
    }

    function whp_fw_python_context(string $root): string {
        $out = ["=== Python/Django context ==="];
        foreach (['requirements.txt', 'pyproject.toml', 'Pipfile'] as $f) {
            if (is_file($root . '/' . $f)) $out[] = "{$f}: present";
        }
        foreach (['manage.py', 'wsgi.py', 'asgi.py'] as $f) {
            if (is_file($root . '/' . $f)) $out[] = "{$f}: present";
        }
        if (is_dir($root . '/venv') || is_dir($root . '/.venv')) $out[] = "virtualenv: present";
        return implode("\n", $out);
    }

    function whp_fw_rails_context(string $root): string {
        $out = ["=== Rails context ==="];
        if (is_file($root . '/Gemfile')) {
            $g = (string)@file_get_contents($root . '/Gemfile');
            if (preg_match("/gem\\s+['\"]rails['\"]\\s*,\\s*['\"]([^'\"]+)['\"]/", $g, $m)) {
                $out[] = "Rails gem version: {$m[1]}";
            }
        }
        foreach (['config/database.yml', 'config/puma.rb', 'config/application.rb'] as $f) {
            if (is_file($root . '/' . $f)) $out[] = "{$f}: present";
        }
        $out[] = "bundle install: " . (is_dir($root . '/vendor/bundle') ? "ran" : "not present");
        return implode("\n", $out);
    }

    function whp_fw_generic_context(string $root): string {
        $out = ["=== Generic site context ==="];
        if (!is_dir($root)) {
            $out[] = "site root not readable at {$root}";
            return implode("\n", $out);
        }
        $entries = @scandir($root) ?: [];
        $entries = array_values(array_filter($entries, fn($e) => $e[0] !== '.'));
        $out[] = "top-level files: " . (count($entries) ? implode(', ', array_slice($entries, 0, 20)) : '(empty)');
        $phpCount  = (int)@trim((string)shell_exec("find " . escapeshellarg($root) . " -maxdepth 2 -name '*.php' 2>/dev/null | wc -l"));
        $htmlCount = (int)@trim((string)shell_exec("find " . escapeshellarg($root) . " -maxdepth 2 -name '*.html' 2>/dev/null | wc -l"));
        $out[] = "files by type: php={$phpCount}, html={$htmlCount}";
        return implode("\n", $out);
    }
}
