<?php

declare(strict_types=1);

namespace App\Support;

use Illuminate\Contracts\Foundation\Application;

/**
 * Self-heals stale compiled caches after a manual (file-copy) update.
 *
 * The in-app updater (UpdaterService) already runs config:clear /
 * route:clear / view:clear after swapping files, but many shared-hosting
 * buyers update by unzipping over the old release and running
 * `php artisan migrate` only. If the host ever compiled its caches
 * (System Health's "rebuild caches" runs route:cache + config:cache),
 * the stale compiled tables keep serving the OLD routes and config —
 * every route added by the update 404s ("Visual builder → Page not
 * found") even though the new code is on disk.
 *
 * Detection: config/leadhub.php ships a new `version` literal with every
 * release. We stamp the version and the file's mtime; when the shipped
 * version changes, the compiled config/route/event caches are deleted
 * once. Views are not touched — Blade recompiles stale views by itself.
 *
 * Runs from AppServiceProvider::register(), i.e. before the boot phase
 * loads routes, so the first healed request already resolves fresh
 * routes. (Config was loaded earlier in bootstrap, so a stale cached
 * config lasts one request longer — gone on the next hit.) The fast
 * path is a single filemtime() stat per request.
 */
class PostUpdateCacheHealer
{
    public static function heal(Application $app): void
    {
        try {
            self::healPaths(
                $app->configPath('leadhub.php'),
                $app->storagePath('framework/leadhub-version.stamp'),
                [
                    $app->getCachedConfigPath(),
                    $app->getCachedRoutesPath(),
                    $app->getCachedEventsPath(),
                ],
            );
        } catch (\Throwable) {
            // Never let cache healing break the boot path.
        }
    }

    /**
     * Pure-path core so tests can exercise the full behaviour against
     * temp files without touching the real bootstrap/cache directory.
     *
     * @param  list<string>  $cachedFiles
     */
    public static function healPaths(string $configFile, string $stampFile, array $cachedFiles): void
    {
        $mtime = @filemtime($configFile);
        if ($mtime === false) {
            return;
        }

        [$stampVersion, $stampMtime] = self::readStamp($stampFile);

        if ($stampMtime === (string) $mtime) {
            return; // fast path — config file untouched since the last check
        }

        $shipped = self::shippedVersion($configFile);

        if ($shipped !== null && $shipped !== $stampVersion) {
            foreach ($cachedFiles as $cached) {
                if (is_file($cached)) {
                    @unlink($cached);
                }
            }
        }

        @file_put_contents($stampFile, ($shipped ?? '') . '|' . $mtime);
    }

    /**
     * Parse the version literal straight off disk — config() may itself be
     * serving a stale cached value, which is exactly the condition we are
     * trying to detect.
     */
    public static function shippedVersion(string $configFile): ?string
    {
        $raw = @file_get_contents($configFile);
        if ($raw === false) {
            return null;
        }

        return preg_match("/'version'\s*=>\s*env\('LEADHUB_VERSION',\s*'([^']+)'\)/", $raw, $m)
            ? $m[1]
            : null;
    }

    /** @return array{0: ?string, 1: ?string} [version, mtime] as stamped */
    private static function readStamp(string $stampFile): array
    {
        $raw = @file_get_contents($stampFile);
        if ($raw === false || ! str_contains($raw, '|')) {
            return [null, null];
        }

        [$version, $mtime] = explode('|', trim($raw), 2);

        return [$version !== '' ? $version : null, $mtime !== '' ? $mtime : null];
    }
}
