<?php

declare(strict_types=1);

final class MyronModulePackageInstallLedger
{
    private const MAX_BYTES = 262144;

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

    /**
     * @return list<array<string, mixed>>
     */
    public function list(): array
    {
        $path = $this->path();
        if (!is_file($path)) {
            return [];
        }
        $contents = (string) file_get_contents($path);
        if ($contents === '' || strlen($contents) > self::MAX_BYTES) {
            return [];
        }

        try {
            $decoded = json_decode($contents, true, 512, JSON_THROW_ON_ERROR);
        } catch (Throwable) {
            return [];
        }

        return is_array($decoded['packages'] ?? null) ? array_values(array_filter($decoded['packages'], 'is_array')) : [];
    }

    /**
     * @return array<string, mixed>|null
     */
    public function findBySku(string $sku): ?array
    {
        $sku = trim($sku);
        if ($sku === '') {
            return null;
        }
        foreach ($this->list() as $entry) {
            if ((string) ($entry['sku'] ?? '') === $sku && (string) ($entry['status'] ?? '') === 'installed') {
                return $entry;
            }
        }

        return null;
    }

    public function hasTrustedInstallForSlug(string $slug, string $sku): bool
    {
        $entry = $this->findBySku($sku);
        if ($entry === null) {
            return false;
        }

        $targetRoots = is_array($entry['targetRoots'] ?? null) ? $entry['targetRoots'] : [];
        $moduleRoot = 'app/modules/' . trim($slug, '/') . '/';
        foreach ($targetRoots as $root) {
            if ((string) $root === $moduleRoot) {
                return true;
            }
        }

        return false;
    }

    public function installedVersionForSku(string $sku): string
    {
        $entry = $this->findBySku($sku);
        return trim((string) ($entry['packageVersion'] ?? ''));
    }

    /**
     * @return list<array{sku:string,packageVersion:string,targetRoots:list<string>,installMode:string}>
     */
    public function pendingBootstrapMigrations(): array
    {
        $pending = [];
        foreach ($this->list() as $entry) {
            if (empty($entry['bootstrapMigrationRequired']) || (string) ($entry['status'] ?? '') !== 'installed') {
                continue;
            }
            $pending[] = [
                'sku' => (string) ($entry['sku'] ?? ''),
                'packageVersion' => (string) ($entry['packageVersion'] ?? ''),
                'targetRoots' => array_values(array_map('strval', is_array($entry['targetRoots'] ?? null) ? $entry['targetRoots'] : [])),
                'installMode' => (string) ($entry['installMode'] ?? ''),
            ];
        }
        return $pending;
    }

    /**
     * Clears migration handoffs only for packages whose complete Module-root set is
     * present in the trusted runtime registry after a successful converging pass.
     *
     * @param list<string> $presentModules
     * @return list<string> reconciled SKU ids
     */
    public function reconcileBootstrapMigrations(array $presentModules): array
    {
        $presentLookup = array_fill_keys(array_map(
            static fn (string $slug): string => trim($slug, '/'),
            array_map('strval', $presentModules)
        ), true);
        $packages = $this->list();
        $reconciled = [];
        $changed = false;
        $now = function_exists('myronNow') ? myronNow() : gmdate('Y-m-d H:i:s');

        foreach ($packages as &$entry) {
            if (empty($entry['bootstrapMigrationRequired']) || (string) ($entry['status'] ?? '') !== 'installed') {
                continue;
            }
            $moduleSlugs = [];
            foreach (is_array($entry['targetRoots'] ?? null) ? $entry['targetRoots'] : [] as $root) {
                $root = trim((string) $root, '/');
                if (str_starts_with($root, 'app/modules/')) {
                    $moduleSlugs[] = substr($root, strlen('app/modules/'));
                }
            }
            if ($moduleSlugs === []) {
                continue;
            }
            foreach ($moduleSlugs as $slug) {
                if (!isset($presentLookup[$slug])) {
                    continue 2;
                }
            }

            $entry['bootstrapMigrationRequired'] = false;
            $entry['migrationsAppliedAt'] = $now;
            $reconciled[] = (string) ($entry['sku'] ?? '');
            $changed = true;
        }
        unset($entry);

        if ($changed) {
            $this->writeContents(json_encode([
                'schemaVersion' => 1,
                'updatedAt' => $now,
                'packages' => $packages,
            ], JSON_THROW_ON_ERROR | JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . PHP_EOL);
        }

        return array_values(array_filter($reconciled, static fn (string $sku): bool => $sku !== ''));
    }

    public function snapshotState(): ?string
    {
        $path = $this->path();
        if (!is_file($path)) {
            return null;
        }

        $contents = file_get_contents($path);
        if (!is_string($contents) || strlen($contents) > self::MAX_BYTES) {
            throw new RuntimeException('Unable to snapshot the Module package ledger.');
        }

        return $contents;
    }

    public function restoreState(?string $contents): void
    {
        $path = $this->path();
        if ($contents === null) {
            if (is_file($path) && !unlink($path)) {
                throw new RuntimeException('Unable to restore the empty Module package ledger state.');
            }
            return;
        }

        $this->writeContents($contents);
    }

    /**
     * @param array<string, mixed> $details
     */
    public function recordInstall(array $details): void
    {
        $sku = trim((string) ($details['marketplaceSku'] ?? ''));
        if ($sku === '') {
            return;
        }

        $entry = [
            'sku' => $sku,
            'packageId' => (string) ($details['packageId'] ?? ''),
            'packageVersion' => (string) ($details['targetVersion'] ?? ''),
            'targetType' => (string) ($details['targetType'] ?? ''),
            'targetRoots' => array_values(array_map('strval', is_array($details['destinationRoots'] ?? null) ? $details['destinationRoots'] : [])),
            'packageHash' => (string) ($details['packageHash'] ?? ''),
            'publisher' => (string) ($details['publisher'] ?? ''),
            'installAuditId' => (string) ($details['auditId'] ?? ''),
            'installMode' => (string) ($details['installMode'] ?? 'installed'),
            'priorVersion' => (string) ($details['priorVersion'] ?? ''),
            'previousPackageHash' => (string) ($details['previousPackageHash'] ?? ''),
            'appliedOperations' => array_values(array_filter(
                is_array($details['appliedOperations'] ?? null) ? $details['appliedOperations'] : [],
                'is_array'
            )),
            'bootstrapMigrationRequired' => (bool) ($details['bootstrapMigrationRequired'] ?? false),
            'installedAt' => function_exists('myronNow') ? myronNow() : gmdate('Y-m-d H:i:s'),
            'status' => 'installed',
        ];

        $packages = [];
        foreach ($this->list() as $existing) {
            if ((string) ($existing['sku'] ?? '') !== $sku) {
                $packages[] = $existing;
            }
        }
        $packages[] = $entry;
        usort($packages, static fn (array $a, array $b): int => strcmp((string) ($a['sku'] ?? ''), (string) ($b['sku'] ?? '')));

        $this->writeContents(json_encode([
            'schemaVersion' => 1,
            'updatedAt' => $entry['installedAt'],
            'packages' => $packages,
        ], JSON_THROW_ON_ERROR | JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . PHP_EOL);
    }

    private function path(): string
    {
        return rtrim($this->appRoot, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . 'storage' . DIRECTORY_SEPARATOR . 'module-packages' . DIRECTORY_SEPARATOR . 'installed.json';
    }

    private function ensureDirectory(string $path): void
    {
        if (!is_dir($path) && !mkdir($path, 0775, true) && !is_dir($path)) {
            throw new RuntimeException('Unable to create directory: ' . $path);
        }
    }

    private function writeContents(string $contents): void
    {
        if ($contents === '' || strlen($contents) > self::MAX_BYTES) {
            throw new RuntimeException('Module package ledger content is invalid.');
        }

        $path = $this->path();
        $this->ensureDirectory(dirname($path));
        $temporaryPath = $path . '.writing.' . bin2hex(random_bytes(6));
        try {
            if (file_put_contents($temporaryPath, $contents, LOCK_EX) === false) {
                throw new RuntimeException('Unable to write the Module package ledger.');
            }
            if (!rename($temporaryPath, $path)) {
                throw new RuntimeException('Unable to publish the Module package ledger.');
            }
        } finally {
            if (is_file($temporaryPath)) {
                @unlink($temporaryPath);
            }
        }
    }
}
