<?php
declare(strict_types=1);

namespace PSLauncher\Tools;

/**
 * Logique de re-signature du manifest, sans dépendance shell.
 * Utilisable depuis le CLI (tools/sign-manifest.php) et depuis le backoffice
 * web (admin/versions.php → action 'sync'). Évite tout appel à exec().
 */
final class SignManifest
{
    public string $manifestPath;
    public string $buildsDir;
    public ?string $configPath;
    /** @var string[] */
    public array $log = [];

    public function __construct(string $rootDir)
    {
        $this->manifestPath = $rootDir . '/manifest/versions.json';
        $this->buildsDir    = $rootDir . '/builds';
        $this->configPath   = $rootDir . '/api/config.php';
    }

    private function out(string $line): void { $this->log[] = $line; }

    /**
     * Recompute sizes / sha256 for every version that has its ZIP uploaded,
     * bump 'latest' to the highest signed version, then sign the manifest
     * with the Ed25519 private key from config.php (if available).
     *
     * @return array{ok:bool, log:string[]}
     */
    public function run(): array
    {
        if (!is_file($this->manifestPath)) {
            $this->out("Manifest introuvable : {$this->manifestPath}");
            return ['ok' => false, 'log' => $this->log];
        }

        $manifest = json_decode(file_get_contents($this->manifestPath), true);
        if (!is_array($manifest)) {
            $this->out("Manifest illisible (JSON invalide)");
            return ['ok' => false, 'log' => $this->log];
        }

        $hashedVersions = [];
        foreach ($manifest['versions'] as &$v) {
            $version = $v['version'] ?? '?';
            $url     = $v['download']['url'] ?? '';
            if ($url === '') {
                $this->out("  [skip] $version : pas d'URL dans le manifest");
                continue;
            }

            $filename = basename(parse_url($url, PHP_URL_PATH) ?: '');
            $zip = "{$this->buildsDir}/$filename";

            if (!is_file($zip)) {
                $candidates = glob("{$this->buildsDir}/*{$version}*.zip", GLOB_NOSORT) ?: [];
                $candidates = array_values(array_filter($candidates, 'is_file'));
                if (count($candidates) === 1) {
                    $zip = $candidates[0];
                    $this->out("  [info] $version : URL pointait vers '{$filename}', utilisé '" . basename($zip) . "' à la place");
                } else {
                    $this->out("  [skip] $version : ZIP introuvable pour $url");
                    if (count($candidates) > 1) {
                        $this->out("         Plusieurs candidats : " . implode(', ', array_map('basename', $candidates)));
                    }
                    continue;
                }
            }

            $size = filesize($zip);
            $sha  = hash_file('sha256', $zip);
            $this->out("  [hash] $version : " . basename($zip) . " ($size octets)  sha256={$sha}");

            $v['download']['sizeBytes'] = $size;
            $v['download']['sha256']    = $sha;
            $hashedVersions[] = $version;
        }
        unset($v);

        // Section launcher : si présente, on tente de hasher le PSLauncher-{ver}.exe
        // dans builds/launcher/. On ignore proprement si l'exe n'est pas là.
        if (isset($manifest['launcher']) && is_array($manifest['launcher'])) {
            $launcher = &$manifest['launcher'];
            $lver = $launcher['version'] ?? '';
            $lurl = $launcher['download']['url'] ?? '';
            if ($lver !== '' && $lurl !== '') {
                $lfile = basename(parse_url($lurl, PHP_URL_PATH) ?: '');
                $lpath = "{$this->buildsDir}/launcher/{$lfile}";
                if (!is_file($lpath)) {
                    // Fallback : tolérant sur le nom (PSLauncher-X.Y.Z.exe, etc.)
                    $candidates = glob("{$this->buildsDir}/launcher/*{$lver}*.exe", GLOB_NOSORT) ?: [];
                    if (count($candidates) === 1) $lpath = $candidates[0];
                }
                if (is_file($lpath)) {
                    $lsize = filesize($lpath);
                    $lsha  = hash_file('sha256', $lpath);
                    $launcher['download']['sizeBytes'] = $lsize;
                    $launcher['download']['sha256']    = $lsha;
                    $this->out("  [launcher] v{$lver} : " . basename($lpath) . " ({$lsize} octets)  sha256={$lsha}");
                } else {
                    $this->out("  [launcher] v{$lver} : exe introuvable dans builds/launcher/");
                }
            }
            unset($launcher);
        }

        // Bump auto de `latest` sur la plus haute version effectivement uploadée
        if (!empty($hashedVersions)) {
            usort($hashedVersions, 'version_compare');
            $newLatest = end($hashedVersions);
            if (($manifest['latest'] ?? null) !== $newLatest) {
                $this->out("  [latest] " . ($manifest['latest'] ?? '(none)') . " -> {$newLatest}");
                $manifest['latest'] = $newLatest;
            }
        }

        // Signature Ed25519
        if ($this->configPath && is_file($this->configPath)) {
            $config = require $this->configPath;
            $sk = $config['ed25519']['private_key_hex'] ?? '';
            if ($sk !== '' && strlen($sk) === 128) {
                $manifest['signature'] = null;
                $cryptoPath = dirname($this->configPath) . '/lib/Crypto.php';
                if (is_file($cryptoPath)) require_once $cryptoPath;
                if (class_exists('\PSLauncher\Crypto')) {
                    $payload = \PSLauncher\Crypto::canonicalJson($manifest);
                    $manifest['signature'] = \PSLauncher\Crypto::signEd25519($payload, $sk);
                    $this->out("  [sign] manifest signé (Ed25519)");
                } else {
                    $this->out("  [warn] Classe \\PSLauncher\\Crypto introuvable, manifest non signé");
                }
            } else {
                $this->out("  [warn] ed25519.private_key_hex non configuré, manifest non signé");
            }
        }

        $jsonOut = json_encode($manifest, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "\n";
        if (file_put_contents($this->manifestPath, $jsonOut) === false) {
            $this->out("Échec d'écriture du manifest : {$this->manifestPath}");
            return ['ok' => false, 'log' => $this->log];
        }

        $this->out("Manifest mis à jour (" . count($hashedVersions) . " version(s)).");
        return ['ok' => true, 'log' => $this->log];
    }
}
