<?php

declare(strict_types=1);

require_once __DIR__ . '/OperatorStateUpdateBoundary.php';

final class MyronOperatorStateSnapshot
{
    /**
     * @param array<string, array{exists: bool, sha256: string, contents: string|null}> $files
     */
    private function __construct(
        private readonly string $projectRoot,
        private readonly array $files,
    ) {
    }

    public static function capture(string $projectRoot): self
    {
        $projectRoot = rtrim($projectRoot, DIRECTORY_SEPARATOR);
        $files = [];

        foreach (MyronOperatorStateUpdateBoundary::protectedSnapshotPaths() as $relativePath) {
            $absolutePath = $projectRoot . DIRECTORY_SEPARATOR . str_replace('/', DIRECTORY_SEPARATOR, $relativePath);
            if (is_dir($absolutePath)) {
                foreach (self::listFiles($absolutePath, $relativePath) as $fileRelative) {
                    $files[$fileRelative] = self::readFileState($projectRoot, $fileRelative);
                }
                continue;
            }

            if (self::isDirectorySnapshotPath($relativePath)) {
                continue;
            }

            $files[$relativePath] = self::readFileState($projectRoot, $relativePath);
        }

        ksort($files);
        return new self($projectRoot, $files);
    }

    /**
     * @return list<string>
     */
    public function changedPaths(): array
    {
        $current = self::capture($this->projectRoot)->files;
        $allPaths = array_values(array_unique(array_merge(array_keys($this->files), array_keys($current))));
        sort($allPaths);

        $changed = [];
        foreach ($allPaths as $path) {
            if (($this->files[$path] ?? null) !== ($current[$path] ?? null)) {
                $changed[] = $path;
            }
        }

        return $changed;
    }

    public function restore(): void
    {
        $current = self::capture($this->projectRoot)->files;
        $allPaths = array_values(array_unique(array_merge(array_keys($this->files), array_keys($current))));
        rsort($allPaths);

        foreach ($allPaths as $path) {
            $original = $this->files[$path] ?? ['exists' => false, 'sha256' => '', 'contents' => null];
            $target = $this->projectRoot . DIRECTORY_SEPARATOR . str_replace('/', DIRECTORY_SEPARATOR, $path);

            if (!$original['exists']) {
                if (is_file($target) || is_link($target)) {
                    unlink($target);
                }
                continue;
            }

            $contents = $original['contents'];
            if (!is_string($contents)) {
                continue;
            }

            $directory = dirname($target);
            if (!is_dir($directory) && !mkdir($directory, 0775, true) && !is_dir($directory)) {
                throw new RuntimeException('Unable to restore protected operator-state directory: ' . $directory);
            }

            if (file_put_contents($target, $contents) === false) {
                throw new RuntimeException('Unable to restore protected operator-state file: ' . $path);
            }
        }
    }

    public function assertUnchanged(): void
    {
        $changed = $this->changedPaths();
        if ($changed === []) {
            return;
        }

        throw new RuntimeException('Update attempted to mutate protected operator-owned state: ' . implode(', ', $changed));
    }

    /**
     * @return list<string>
     */
    private static function listFiles(string $absoluteRoot, string $relativeRoot): array
    {
        $paths = [];
        $iterator = new RecursiveIteratorIterator(
            new RecursiveDirectoryIterator($absoluteRoot, FilesystemIterator::SKIP_DOTS)
        );
        foreach ($iterator as $item) {
            if (!$item->isFile()) {
                continue;
            }

            $relative = MyronOperatorStateUpdateBoundary::normalizePath(
                $relativeRoot . '/' . substr((string) $item->getPathname(), strlen($absoluteRoot) + 1)
            );
            if (str_starts_with($relative, 'app/storage/updates/')) {
                continue;
            }
            $paths[] = $relative;
        }

        sort($paths);
        return $paths;
    }

    /**
     * @return array{exists: bool, sha256: string, contents: string|null}
     */
    private static function readFileState(string $projectRoot, string $relativePath): array
    {
        $path = $projectRoot . DIRECTORY_SEPARATOR . str_replace('/', DIRECTORY_SEPARATOR, $relativePath);
        if (!is_file($path)) {
            return ['exists' => false, 'sha256' => '', 'contents' => null];
        }

        $contents = (string) file_get_contents($path);
        return [
            'exists' => true,
            'sha256' => hash('sha256', $contents),
            'contents' => $contents,
        ];
    }

    private static function isDirectorySnapshotPath(string $relativePath): bool
    {
        return in_array($relativePath, [
            'app/jsonm/definitions',
            'app/uploads',
            'app/system/uploads',
            'app/storage',
            'app/assets/media',
        ], true);
    }
}
