<?php

declare(strict_types=1);

final class MyronPublicPageCacheWarmCoordinator
{
    private const STATE_SCHEMA = 1;
    private const DEFAULT_LIMIT = 25;
    private const DEFAULT_TIMEOUT_SECONDS = 5;
    private const MAX_REASONS = 10;
    private const STATE_FILE = 'warm-state.json';
    private const LOCK_FILE = 'warm.lock';

    private static bool $shutdownScheduled = false;

    public function __construct(
        private readonly string $projectRoot,
        private readonly string $cacheRoot
    ) {
    }

    public static function fromProjectRoot(string $projectRoot): self
    {
        $root = rtrim($projectRoot, DIRECTORY_SEPARATOR);
        return new self($root, $root . '/app/storage/cache/public-pages');
    }

    /**
     * @return array<string, mixed>
     */
    public function request(string $reason = ''): array
    {
        $this->ensureCacheRoot();
        $state = $this->readState();
        $reasons = array_values(array_filter(
            is_array($state['reasons'] ?? null) ? $state['reasons'] : [],
            static fn (mixed $value): bool => is_string($value) && trim($value) !== ''
        ));
        $reason = trim($reason);
        if ($reason !== '') {
            $reasons[] = $reason;
        }
        $reasons = array_values(array_unique($reasons));
        if (count($reasons) > self::MAX_REASONS) {
            $reasons = array_slice($reasons, -self::MAX_REASONS);
        }

        $state = [
            'schema' => self::STATE_SCHEMA,
            'pending' => true,
            'requestId' => bin2hex(random_bytes(12)),
            'requestCount' => max(0, (int) ($state['requestCount'] ?? 0)) + 1,
            'requestedAt' => gmdate('c'),
            'reasons' => $reasons,
            'lastAttemptAt' => (string) ($state['lastAttemptAt'] ?? ''),
            'lastCompletedAt' => (string) ($state['lastCompletedAt'] ?? ''),
            'lastResult' => is_array($state['lastResult'] ?? null) ? $state['lastResult'] : [],
        ];
        $this->writeState($state);
        return $state;
    }

    public function schedulePostResponseWarm(
        int $limit = self::DEFAULT_LIMIT,
        int $timeoutSeconds = self::DEFAULT_TIMEOUT_SECONDS
    ): bool {
        if (
            self::$shutdownScheduled
            || PHP_SAPI === 'cli'
            || !function_exists('fastcgi_finish_request')
            || !class_exists(MyronPublicPageCacheWarmer::class)
        ) {
            return false;
        }

        self::$shutdownScheduled = true;
        register_shutdown_function(function () use ($limit, $timeoutSeconds): void {
            try {
                @fastcgi_finish_request();
                $this->run(
                    MyronPublicPageCacheWarmer::fromProjectRoot($this->projectRoot),
                    $limit,
                    $timeoutSeconds,
                    false
                );
            } catch (Throwable) {
                // Post-response warming is opportunistic; the CLI is the safety net.
            }
        });

        return true;
    }

    /**
     * @return array<string, mixed>
     */
    public function run(
        MyronPublicPageCacheWarmer $warmer,
        int $limit = self::DEFAULT_LIMIT,
        int $timeoutSeconds = self::DEFAULT_TIMEOUT_SECONDS,
        bool $force = false
    ): array {
        $limit = max(1, $limit);
        $timeoutSeconds = max(1, $timeoutSeconds);
        $this->ensureCacheRoot();

        $lock = @fopen($this->lockPath(), 'c+');
        if (!is_resource($lock)) {
            return $this->failureResult('lock-unavailable', 'Unable to open the public cache warm lock.');
        }
        if (!@flock($lock, LOCK_EX | LOCK_NB)) {
            fclose($lock);
            return [
                'ok' => true,
                'coordination' => 'locked',
                'pending' => (bool) ($this->readState()['pending'] ?? false),
                'attempted' => 0,
                'warmed' => 0,
                'failed' => 0,
                'routes' => [],
            ];
        }

        try {
            $initial = $this->readState();
            $hadPendingRequest = (bool) ($initial['pending'] ?? false);
            if (!$hadPendingRequest && !$force) {
                return [
                    'ok' => true,
                    'coordination' => 'idle',
                    'pending' => false,
                    'attempted' => 0,
                    'warmed' => 0,
                    'failed' => 0,
                    'routes' => [],
                ];
            }

            $attemptedAt = gmdate('c');
            $result = $warmer->warm(null, $limit, $timeoutSeconds);
            $latest = $this->readState();
            $sameRequest = !$hadPendingRequest || (
                (string) ($initial['requestId'] ?? '') !== ''
                && hash_equals((string) ($initial['requestId'] ?? ''), (string) ($latest['requestId'] ?? ''))
            );
            $completed = !empty($result['ok']) && $sameRequest;

            $state = $latest;
            $state['schema'] = self::STATE_SCHEMA;
            $state['pending'] = !$completed && (bool) ($latest['pending'] ?? $hadPendingRequest);
            $state['lastAttemptAt'] = $attemptedAt;
            if ($completed) {
                $state['lastCompletedAt'] = gmdate('c');
            }
            $state['lastResult'] = [
                'ok' => (bool) ($result['ok'] ?? false),
                'attempted' => (int) ($result['attempted'] ?? 0),
                'warmed' => (int) ($result['warmed'] ?? 0),
                'failed' => (int) ($result['failed'] ?? 0),
            ];
            $this->writeState($state);

            return $result + [
                'coordination' => $completed ? 'completed' : 'pending',
                'pending' => (bool) $state['pending'],
            ];
        } catch (Throwable $exception) {
            return $this->failureResult('failed', $exception->getMessage());
        } finally {
            @flock($lock, LOCK_UN);
            fclose($lock);
        }
    }

    /**
     * @return array<string, mixed>
     */
    public function state(): array
    {
        return $this->readState();
    }

    private function statePath(): string
    {
        return $this->cacheRoot . '/' . self::STATE_FILE;
    }

    private function lockPath(): string
    {
        return $this->cacheRoot . '/' . self::LOCK_FILE;
    }

    /**
     * @return array<string, mixed>
     */
    private function readState(): array
    {
        $path = $this->statePath();
        if (!is_file($path)) {
            return [];
        }

        try {
            $json = file_get_contents($path);
            $decoded = is_string($json) ? json_decode($json, true, 512, JSON_THROW_ON_ERROR) : null;
            return is_array($decoded) && (int) ($decoded['schema'] ?? 0) === self::STATE_SCHEMA ? $decoded : [];
        } catch (Throwable) {
            return [];
        }
    }

    /**
     * @param array<string, mixed> $state
     */
    private function writeState(array $state): void
    {
        $this->ensureCacheRoot();
        $contents = json_encode($state, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR) . "\n";
        $path = $this->statePath();
        $tmp = $path . '.tmp.' . bin2hex(random_bytes(6));
        if (file_put_contents($tmp, $contents, LOCK_EX) !== strlen($contents)) {
            @unlink($tmp);
            throw new RuntimeException('Unable to write public cache warm state.');
        }
        if (!rename($tmp, $path)) {
            @unlink($tmp);
            throw new RuntimeException('Unable to publish public cache warm state.');
        }
    }

    private function ensureCacheRoot(): void
    {
        if (is_dir($this->cacheRoot)) {
            return;
        }
        if (function_exists('myronEnsureDirectory')) {
            myronEnsureDirectory($this->cacheRoot);
            return;
        }
        if (!mkdir($this->cacheRoot, 0775, true) && !is_dir($this->cacheRoot)) {
            throw new RuntimeException('Unable to create public page cache directory.');
        }
    }

    /**
     * @return array<string, mixed>
     */
    private function failureResult(string $coordination, string $error): array
    {
        return [
            'ok' => false,
            'coordination' => $coordination,
            'pending' => (bool) ($this->readState()['pending'] ?? false),
            'attempted' => 0,
            'warmed' => 0,
            'failed' => 1,
            'routes' => [['route' => '', 'ok' => false, 'status' => 0, 'error' => $error]],
        ];
    }
}

if (!function_exists('myronPublicPageCacheWarmCoordinator')) {
    function myronPublicPageCacheWarmCoordinator(): MyronPublicPageCacheWarmCoordinator
    {
        static $coordinator = null;
        if ($coordinator instanceof MyronPublicPageCacheWarmCoordinator) {
            return $coordinator;
        }

        $coordinator = MyronPublicPageCacheWarmCoordinator::fromProjectRoot(dirname(__DIR__, 3));
        return $coordinator;
    }
}

if (!function_exists('myronRunCoordinatedPublicPageCacheWarmFailSoft')) {
    /**
     * @return array<string, mixed>
     */
    function myronRunCoordinatedPublicPageCacheWarmFailSoft(
        int $limit = 25,
        int $timeoutSeconds = 5,
        bool $force = true
    ): array {
        try {
            return myronPublicPageCacheWarmCoordinator()->run(
                myronPublicPageCacheWarmer(),
                $limit,
                $timeoutSeconds,
                $force
            );
        } catch (Throwable $exception) {
            return [
                'ok' => false,
                'coordination' => 'failed',
                'pending' => true,
                'attempted' => 0,
                'warmed' => 0,
                'failed' => 1,
                'routes' => [['route' => '', 'ok' => false, 'status' => 0, 'error' => $exception->getMessage()]],
            ];
        }
    }
}
