<?php

declare(strict_types=1);

final class MyronEntitlementBoundaryIntegrityService
{
    /**
     * @return list<string>
     */
    public static function protectedFiles(): array
    {
        return [
            'app/admin/routes/administration/license-manager.php',
            'app/api/actions/administration/InstallModulePackageAction.php',
            'app/api/actions/administration/RedeemModuleLicenseAction.php',
            'app/system/license/EntitlementGate.php',
            'app/system/license/EntitlementPresentationPolicy.php',
            'app/system/license/ModuleLicenseManagerService.php',
            'app/system/modules/ModuleLoader.php',
            'app/system/modules/ModuleRegistry.php',
            'app/system/update/EntitlementBoundaryIntegrityService.php',
            'app/system/update/MarketplaceEnvelopeVerifier.php',
            'app/system/update/MarketplaceKeyResolver.php',
            'app/system/update/ModulePackageInstallLedger.php',
            'app/system/update/RemoteModulePackageClient.php',
            'app/system/update/RemoteUpdateClient.php',
            'app/system/update/RemoteUpdateCheckService.php',
            'app/system/update/UpdateApplicationService.php',
            'app/system/update/UpdatePackageValidator.php',
        ];
    }

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

    /**
     * @return array<string, mixed>
     */
    public function snapshot(): array
    {
        $hashes = [];
        $missing = [];
        foreach (self::protectedFiles() as $relative) {
            $path = rtrim($this->projectRoot, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . str_replace('/', DIRECTORY_SEPARATOR, $relative);
            if (!is_file($path)) {
                $missing[] = $relative;
                continue;
            }
            $hash = hash_file('sha256', $path);
            $hashes[$relative] = is_string($hash) ? $hash : '';
        }

        $expected = $this->expectedHashes();
        $mismatches = [];
        foreach ($expected as $relative => $expectedHash) {
            if (($hashes[$relative] ?? '') !== $expectedHash) {
                $mismatches[] = $relative;
            }
        }

        $status = ($missing === [] && $mismatches === []) ? 'ok' : 'failed';
        return [
            'schemaVersion' => 1,
            'status' => $status,
            'mode' => $expected === [] ? 'observe_only' : 'enforced',
            'protectedFileCount' => count(self::protectedFiles()),
            'hashes' => $hashes,
            'missing' => $missing,
            'mismatches' => $mismatches,
        ];
    }

    /**
     * @return array<string, string>
     */
    private function expectedHashes(): array
    {
        $raw = trim((string) getenv('MYRON_ENTITLEMENT_BOUNDARY_HASHES_JSON'));
        if ($raw === '') {
            return [];
        }
        $decoded = json_decode($raw, true);
        if (!is_array($decoded)) {
            return [];
        }

        $hashes = [];
        foreach ($decoded as $path => $hash) {
            $path = trim((string) $path);
            $hash = strtolower(trim((string) $hash));
            if ($path !== '' && preg_match('/^[a-f0-9]{64}$/', $hash) === 1) {
                $hashes[$path] = $hash;
            }
        }

        return $hashes;
    }
}
