<?php

declare(strict_types=1);

namespace PpPhp\Core\Projects;

/** Extracts a previously inspected package into a new destination atomically. */
final class ProjectPackageImporter
{
    public function import(string $archive, string $destination, bool $allowCode): array
    {
        if (file_exists($destination)) throw new \RuntimeException('The destination project directory already exists.');
        $parent = dirname($destination);
        if (!is_dir($parent) && !mkdir($parent, 0755, true) && !is_dir($parent)) throw new \RuntimeException('Could not create the project owner directory.');
        $temporary = $parent . '/.import-' . bin2hex(random_bytes(10));
        if (!mkdir($temporary, 0700, true)) throw new \RuntimeException('Could not prepare the atomic import directory.');

        $zip = new \ZipArchive();
        if ($zip->open($archive, \ZipArchive::RDONLY) !== true) {
            $this->removeDirectory($temporary);
            throw new \RuntimeException('The inspected package can no longer be opened.');
        }
        $written = [];
        try {
            for ($index = 0; $index < $zip->numFiles; $index++) {
                $stat = $zip->statIndex($index);
                $raw = (string)($stat['name'] ?? '');
                if ($raw === '' || str_ends_with($raw, '/')) continue;
                $entry = ProjectPackageFormat::normalizeEntryPath($raw);
                if ($entry === 'manifest.json' || $entry === 'project/connection.json') continue;
                if (!ProjectPackageFormat::isAllowedEntry($entry)) throw new \RuntimeException('Unsupported package path: ' . $entry);
                if (ProjectPackageFormat::isExecutableEntry($entry) && !$allowCode) {
                    throw new \RuntimeException('This package contains custom executable files. Confirm the code review option before importing.');
                }
                $relative = substr($entry, strlen('project/'));
                $target = $temporary . '/' . $relative;
                if (!is_dir(dirname($target)) && !mkdir(dirname($target), 0700, true) && !is_dir(dirname($target))) throw new \RuntimeException('Could not prepare an imported folder.');
                $input = $zip->getStream($raw);
                $output = @fopen($target, 'wb');
                if (!is_resource($input) || !is_resource($output)) throw new \RuntimeException('Could not extract ' . $entry . '.');
                try {
                    if (stream_copy_to_stream($input, $output, ProjectPackageFormat::MAX_EXPANDED_BYTES + 1) === false) throw new \RuntimeException('Could not extract ' . $entry . '.');
                } finally {
                    fclose($input); fclose($output);
                }
                @chmod($target, ProjectPackageFormat::isExecutableEntry($entry) ? 0640 : 0644);
                $written[] = $relative;
            }
            if (!is_dir($temporary . '/config')) mkdir($temporary . '/config', 0700, true);
            if (!rename($temporary, $destination)) throw new \RuntimeException('Could not publish the imported project atomically.');
            @chmod($destination, 0755);
            return ['files' => $written, 'sqlite_included' => is_file($destination . '/database/app.sqlite')];
        } catch (\Throwable $e) {
            $this->removeDirectory($temporary);
            throw $e;
        } finally {
            $zip->close();
        }
    }

    public function rollback(string $destination): void { $this->removeDirectory($destination); }

    private function removeDirectory(string $dir): void
    {
        if (!is_dir($dir)) return;
        $iterator = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($dir, \FilesystemIterator::SKIP_DOTS), \RecursiveIteratorIterator::CHILD_FIRST);
        foreach ($iterator as $item) $item->isDir() ? @rmdir($item->getPathname()) : @unlink($item->getPathname());
        @rmdir($dir);
    }
}
