<?php

declare(strict_types=1);

namespace PpPhp\Core;

/**
 * The version of the PPPHP platform CODE in this working copy.
 *
 * This is the single source of truth for releases: the packager stamps it into
 * every bundle's release.json, and the updater compares a bundle against the
 * version recorded on the server to decide whether an update is an upgrade, a
 * re-apply or a downgrade.
 *
 * This is the ONE version the whole app shows: `PlatformSettings::version()`
 * (sidebar footer, Projects badge), the App Bundle / Update module, and the
 * updater all read this constant. It is bumped automatically forward when you
 * Publish (OTA) — `Version::stampCode()` rewrites it — or when the owner edits
 * the version in Settings. You rarely need to touch it by hand.
 */
final class Version
{
    /** Semantic version of the platform code: MAJOR.MINOR.PATCH.
     *  Bump this when you cut a release, so the dev copy, the bundles you build,
     *  and the servers you deploy to all report the same version. */
    public const PLATFORM = '1.0.103';

    /** Human-readable release name shown in the updater. */
    public const CODENAME = 'First Light';

    /** Minimum PHP the release supports — the updater refuses to install below this. */
    public const MIN_PHP = '8.1.0';

    /** Compare two version strings the way the updater does. */
    public static function compare(string $a, string $b): int
    {
        return version_compare(self::normalise($a), self::normalise($b));
    }

    /** Tolerate "1.0", "v1.0.0", " 1.0.0 " → "1.0.0". */
    public static function normalise(string $v): string
    {
        $v = trim($v);
        $v = ltrim($v, 'vV');
        if ($v === '') return '0.0.0';
        $parts = array_slice(array_map('intval', explode('.', $v)), 0, 3);
        while (count($parts) < 3) $parts[] = 0;
        return implode('.', $parts);
    }

    /**
     * Stamp the code version FORWARD to $v by rewriting the PLATFORM constant in
     * this file. Called after a live (OTA) publish: the machine that publishes a
     * release built the bundle from its own working copy, so it IS that version —
     * the Update section then shows the published build as current and never
     * offers this machine an update to a build it produced.
     *
     * Forward-only (never downgrades), no-ops if already at/ahead of $v, and
     * fails safe (returns false) if the file is not writable.
     */
    public static function stampCode(string $v): bool
    {
        $v = self::normalise($v);
        if (self::compare($v, self::PLATFORM) <= 0) return false; // already at/ahead
        $file = __FILE__;
        $src = @file_get_contents($file);
        if ($src === false) return false;
        $new = preg_replace(
            "/(const PLATFORM = ')\\d+\\.\\d+\\.\\d+(';)/",
            '${1}' . $v . '${2}',
            $src,
            1,
            $count
        );
        if ($new === null || $count !== 1) return false;
        if (@file_put_contents($file, $new, LOCK_EX) === false) return false;
        // Drop any cached bytecode so the new constant is read on the next request.
        if (function_exists('opcache_invalidate')) @opcache_invalidate($file, true);
        return true;
    }
}
