<?php

declare(strict_types=1);

namespace PpPhp\Core\Update;

use PpPhp\Core\Notification;
use PpPhp\Core\PlatformSettings;

/**
 * Automatic, hands-off "an update is waiting" awareness.
 *
 * Distinct from {@see AutoUpdater} (opt-in unattended check-AND-apply, cron-driven):
 * this only CHECKS the signed release feed and, when a newer version appears, drops
 * a notification in the console bell for the administrators — it never changes a
 * file. So a licensed copy always knows the latest is available and the admin
 * decides when to apply it (via OTA "Check for Updates" or "Update Locally").
 *
 * Runs opportunistically on ordinary page loads (rate-limited to once every few
 * hours) and AFTER the response has been flushed, so no visitor ever waits on the
 * network fetch. Silent on every failure — awareness is a nicety, never a blocker.
 */
final class UpdateNotifier
{
    private const SETTING  = 'platform.update_notify';
    private const INTERVAL = 21600;   // check at most once every 6 h

    /** Opportunistic hook for public/index.php — cheap, rate-limited, deferred. */
    public static function tickIfDue(): void
    {
        try {
            $s = self::state();
            if ((time() - (int) ($s['lastCheck'] ?? 0)) < self::INTERVAL) return;
            if (!self::eligible()) return;
            self::save(['lastCheck' => time()]);   // claim the slot so concurrent loads don't double-check

            $run = static function (): void {
                // Two check-ins ride this one deferred slot: "is a newer release
                // out?" (the update feed) and "any Owner-pushed advisories for me?"
                // (the authenticated licensing channel). Each is independent and
                // silent on failure.
                try { (new self())->check(); } catch (\Throwable $e) { /* ambient */ }
                try { if (class_exists(AdvisoryClient::class)) (new AdvisoryClient())->pull(); }
                catch (\Throwable $e) { /* ambient */ }
            };

            // Do the network fetch AFTER the page is sent, so nobody waits on it.
            if (function_exists('fastcgi_finish_request') || function_exists('litespeed_finish_request')) {
                register_shutdown_function(static function () use ($run): void {
                    @ignore_user_abort(true);
                    if (function_exists('fastcgi_finish_request')) { @fastcgi_finish_request(); }
                    elseif (function_exists('litespeed_finish_request')) { @litespeed_finish_request(); }
                    $run();
                });
            } else {
                $run();
            }
        } catch (\Throwable $e) {
            // never let update awareness disturb a page load
        }
    }

    /**
     * Check the feed and, if a newer version is available and not already announced,
     * notify the administrators. Returns a small verdict for callers/tests.
     *
     * @return array{available:bool,version?:string,notified?:bool}
     */
    public function check(): array
    {
        try {
            $res = (new UpdateClient())->check();
        } catch (\Throwable $e) {
            return ['available' => false];
        }
        $available = !empty($res['available']) || !empty($res['fullOnly']);
        $version   = (string) ($res['version'] ?? '');
        if (!$available || $version === '') {
            self::save(['available' => '']);
            return ['available' => false];
        }
        $already = (string) (self::state()['notified'] ?? '');
        if ($already === $version) {
            self::save(['available' => $version]);
            return ['available' => true, 'version' => $version, 'notified' => false];
        }
        self::notify($version);
        self::save(['available' => $version, 'notified' => $version]);
        return ['available' => true, 'version' => $version, 'notified' => true];
    }

    /** Drop the "update available" item in the bell for everyone who can update. */
    private static function notify(string $version): void
    {
        if (!class_exists(Notification::class)) return;
        $data = [
            'title'   => 'Platform update available',
            'message' => 'Version ' . $version . ' is ready — open Platform Updates to install it.',
            'url'     => '/platform/updates',
            'type'    => 'info',
            'icon'    => 'rocket-takeoff',
        ];
        foreach (['superadmin', 'owner'] as $role) {
            try { Notification::AddForRole($role, $data); } catch (\Throwable $e) { /* one role failing is fine */ }
        }
    }

    /**
     * Only a GENUINE, currently-VALID licence may inquire the update host.
     *
     * LicenseGate::locked() is the authoritative verdict — it is true for a copy
     * whose licence is expired, revoked, absent (NONE), unreadable (INVALID), or
     * whose enforcement files were TAMPERED, and false for a live licence (incl.
     * the server-couldn't-be-reached GRACE window, which is still legitimate). So
     * a client only ever reaches out for updates while its licence — as issued and
     * renewed by the licence server — actually checks out. On a non-enforced
     * authority/dev copy locked() is false, which is also correct.
     *
     * We deliberately do NOT require UpdateHostConfig here — that class is
     * server-only; a client resolves the feed via UpdateTrust/UpdateMirrors, and
     * UpdateClient::check() itself no-ops cheaply when no index is configured.
     */
    private static function eligible(): bool
    {
        try {
            if (class_exists(\PpPhp\Core\Licensing\LicenseGate::class)
                && \PpPhp\Core\Licensing\LicenseGate::locked()) {
                return false;   // no genuine, valid licence — stay quiet
            }
            return true;
        } catch (\Throwable $e) {
            return false;
        }
    }

    /** The version currently flagged as available (for a console badge), or ''. */
    public static function available(): string
    {
        return (string) (self::state()['available'] ?? '');
    }

    /** @return array<string,mixed> */
    private static function state(): array
    {
        try {
            $v = PlatformSettings::get(self::SETTING)['value'] ?? [];
            return is_array($v) ? $v : [];
        } catch (\Throwable $e) {
            return [];
        }
    }

    private static function save(array $patch): void
    {
        try { PlatformSettings::set(self::SETTING, ['value' => $patch + self::state()]); }
        catch (\Throwable $e) { /* best effort */ }
    }
}
