<?php

declare(strict_types=1);

namespace PpPhp\Core\Update;

use PpPhp\Core\Version;

/**
 * Remembers the file manifest of each FULL bundle the owner builds, so a later
 * "Updates only" patch can diff against it — without re-uploading or re-building
 * the previous release.
 *
 * A baseline is just the {relative-path => sha256} map that a full bundle already
 * carries in its release.json. We snapshot it, keyed by EDITION and VERSION,
 * because a delta is only correct between two bundles of the same edition (a
 * client baseline lacks the server-only files, and vice-versa).
 *
 * Authority-only: it participates in producing releases, so the packager keeps it
 * out of client bundles.
 *
 * Layout:  storage/updates/baselines/<edition>/<version>.json
 */
final class BaselineStore
{
    private string $root;

    public function __construct(?string $root = null)
    {
        $this->root = rtrim($root ?? (string) constant('BASE_PATH'), '/');
    }

    private function dir(string $edition): string
    {
        return $this->root . '/storage/updates/baselines/' . self::edition($edition);
    }

    /**
     * Record a full build's manifest as a baseline for future patches.
     *
     * @param array<string,string> $files  release.json's {rel: sha256} (a stray
     *                                      __version__ key is tolerated + dropped)
     */
    public function record(string $edition, string $version, array $files, array $meta = []): bool
    {
        $edition = self::edition($edition);
        $version = Version::normalise($version);
        if ($version === '' || $version === '0') return false;

        unset($files['__version__']);
        $dir = $this->dir($edition);
        if (!is_dir($dir) && !@mkdir($dir, 0775, true) && !is_dir($dir)) return false;

        $payload = [
            'version'   => $version,
            'edition'   => $edition,
            'builtAt'   => date('c'),
            'fileCount' => count($files),
            'files'     => $files,
        ] + $meta;

        return @file_put_contents(
            $dir . '/' . $version . '.json',
            json_encode($payload, JSON_UNESCAPED_SLASHES)
        ) !== false;
    }

    /**
     * Baselines available for an edition, newest first.
     *
     * @return array<int,array{version:string,builtAt:string,fileCount:int}>
     */
    public function list(string $edition): array
    {
        $out = [];
        foreach ((array) glob($this->dir($edition) . '/*.json') as $f) {
            $j = json_decode((string) @file_get_contents($f), true);
            if (!is_array($j) || empty($j['version'])) continue;
            $out[] = [
                'version'   => (string) $j['version'],
                'builtAt'   => (string) ($j['builtAt'] ?? ''),
                'fileCount' => (int) ($j['fileCount'] ?? 0),
            ];
        }
        usort($out, static fn ($a, $b) => version_compare($b['version'], $a['version']));
        return $out;
    }

    public function has(string $edition, string $version): bool
    {
        return is_file($this->dir($edition) . '/' . Version::normalise($version) . '.json');
    }

    /**
     * The baseline manifest for delta building — {rel: sha256} plus the reserved
     * `__version__` so DeltaBuilder can label the patch's "from".
     *
     * @return array<string,string>
     */
    public function manifest(string $edition, string $version): array
    {
        $version = Version::normalise($version);
        $f = $this->dir($edition) . '/' . $version . '.json';
        if (!is_file($f)) return [];
        $j = json_decode((string) @file_get_contents($f), true);
        $files = (is_array($j) && is_array($j['files'] ?? null)) ? $j['files'] : [];
        if ($files === []) return [];
        $files['__version__'] = $version;
        return $files;
    }

    private static function edition(string $e): string
    {
        return strtolower(trim($e)) === 'server' ? 'server' : 'client';
    }
}
