<?php

declare(strict_types=1);

namespace PpPhp\Core\Projects;

use PpPhp\Core\DatabaseConnection;
use PpPhp\Core\ProjectDatabaseProfile;

/** Creates a credential-free, checksummed editable-project package. */
final class ProjectPackageExporter
{
    public function export(array $project, string $projectDir, array $connection, array $options = []): array
    {
        if (!class_exists(\ZipArchive::class)) throw new \RuntimeException('The PHP ZIP extension is required for project export.');
        if (!is_dir($projectDir)) throw new \RuntimeException('The editable project directory was not found.');

        $includeCustom = $this->toBool($options['include_custom'] ?? true);
        $includeSqlite = $this->toBool($options['include_sqlite'] ?? false);
        $files = [];
        $warnings = [];
        $secretsRemoved = 0;

        $configDir = rtrim($projectDir, '/') . '/config';
        foreach (glob($configDir . '/*.json') ?: [] as $path) {
            // Only bundle config files the importer will accept (its allow-list
            // rejects names not starting alphanumerically, e.g. the transient
            // `_resume.json` UI state) — keep export and import symmetric.
            if (!ProjectPackageFormat::isAllowedEntry('project/config/' . basename($path))) {
                continue;
            }
            $raw = file_get_contents($path);
            $decoded = $raw === false ? null : json_decode($raw, true);
            if (!is_array($decoded)) {
                $warnings[] = basename($path) . ' was skipped because it is not valid JSON.';
                continue;
            }
            $clean = ProjectPackageFormat::sanitizeJson($decoded, $secretsRemoved);
            $files['project/config/' . basename($path)] = json_encode($clean, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) . "\n";
        }

        $publicConnection = ProjectDatabaseProfile::publicPayload($connection);
        unset($publicConnection['password_saved']);
        $files['project/connection.json'] = json_encode([
            'type' => $publicConnection['type'],
            'label' => DatabaseConnection::label((string)$publicConnection['type']),
            'schema' => $publicConnection['schema'],
            'charset' => $publicConnection['charset'],
            'credentials_included' => false,
        ], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "\n";

        if ($this->toBool($options['capture_schema'] ?? true)) {
            try {
                $blueprint = (new ProjectSchemaBlueprintService())->capture($projectDir, $connection);
                $files['project/config/schema-blueprint.json'] = json_encode($blueprint, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) . "\n";
            } catch (\Throwable $schemaError) {
                $warnings[] = 'Portable schema capture was skipped: ' . $schemaError->getMessage();
            }
        }

        if ($includeCustom && is_dir($projectDir . '/custom')) {
            $iterator = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($projectDir . '/custom', \FilesystemIterator::SKIP_DOTS));
            foreach ($iterator as $file) {
                if (!$file instanceof \SplFileInfo || !$file->isFile() || $file->isLink()) continue;
                $relative = str_replace('\\', '/', substr($file->getPathname(), strlen($projectDir . '/custom/') ));
                $entry = 'project/custom/' . ltrim($relative, '/');
                if (!ProjectPackageFormat::isAllowedEntry($entry)) continue;
                $files[$entry] = ['path' => $file->getPathname(), 'size' => (int)$file->getSize()];
            }
        }

        $normalizedConnection = DatabaseConnection::normalize($connection);
        if ($includeSqlite && $normalizedConnection['type'] === 'sqlite') {
            $sqlitePath = DatabaseConnection::resolveSqlitePath($normalizedConnection['path'], $projectDir);
            if ($sqlitePath !== ':memory:' && is_file($sqlitePath)) {
                $files['project/database/app.sqlite'] = ['path' => $sqlitePath, 'size' => (int)filesize($sqlitePath)];
            } else {
                $warnings[] = 'The SQLite database file was requested but could not be found.';
            }
        }

        if ($secretsRemoved > 0) $warnings[] = $secretsRemoved . ' saved secret value(s) were removed from exported JSON settings.';
        if (isset($files['project/config/events.json']) || $includeCustom) {
            $warnings[] = 'Review Events and Custom Files before sharing: secret literals embedded inside source code cannot be identified reliably.';
        }

        $checksums = [];
        $expandedBytes = 0;
        $executableCount = 0;
        foreach ($files as $entry => $source) {
            if (ProjectPackageFormat::isExecutableEntry($entry)) $executableCount++;
            if (is_array($source)) {
                $checksums[$entry] = hash_file('sha256', $source['path']);
                $expandedBytes += (int)$source['size'];
            } else {
                $checksums[$entry] = hash('sha256', $source);
                $expandedBytes += strlen($source);
            }
        }
        if ($expandedBytes > ProjectPackageFormat::MAX_EXPANDED_BYTES) {
            throw new \RuntimeException('The selected project content exceeds the ' . ProjectPackageFormat::formatBytes(ProjectPackageFormat::MAX_EXPANDED_BYTES) . ' package limit.');
        }

        $manifest = [
            'format' => ProjectPackageFormat::ID,
            'format_version' => ProjectPackageFormat::VERSION,
            'exported_at' => date(DATE_ATOM),
            'generator' => ['name' => 'PPphp', 'package' => 'projects-phase-2'],
            'project' => [
                'name' => (string)($project['name'] ?? ''),
                'slug' => (string)($project['slug'] ?? ''),
                'description' => (string)($project['description'] ?? ''),
                'color' => (string)($project['color'] ?? '#4f46e5'),
                'icon' => (string)($project['icon'] ?? 'folder2-open'),
                // Optional template version — stamped so a built-in package is
                // self-describing (inspect the .ppphp and see which build it is).
                // Empty for ordinary project exports that carry no version.
                'version' => (string)($options['version'] ?? $project['version'] ?? ''),
            ],
            'database' => [
                'type' => $normalizedConnection['type'],
                'label' => DatabaseConnection::label($normalizedConnection['type']),
                'sqlite_included' => isset($files['project/database/app.sqlite']),
                'credentials_included' => false,
            ],
            'content' => [
                'configuration_files' => count(array_filter(array_keys($files), static fn(string $p): bool => str_starts_with($p, 'project/config/'))),
                'custom_files' => count(array_filter(array_keys($files), static fn(string $p): bool => str_starts_with($p, 'project/custom/'))),
                'executable_custom_files' => $executableCount,
                'expanded_bytes' => $expandedBytes,
            ],
            'warnings' => $warnings,
            'checksums' => $checksums,
        ];

        $temp = tempnam(sys_get_temp_dir(), 'ppphp-export-');
        if ($temp === false) throw new \RuntimeException('Could not prepare a temporary export file.');
        $archive = $temp . '.' . ProjectPackageFormat::EXTENSION;
        @unlink($temp);
        $zip = new \ZipArchive();
        if ($zip->open($archive, \ZipArchive::CREATE | \ZipArchive::OVERWRITE) !== true) throw new \RuntimeException('Could not create the project package.');
        try {
            foreach ($files as $entry => $source) {
                $ok = is_array($source) ? $zip->addFile($source['path'], $entry) : $zip->addFromString($entry, $source);
                if (!$ok) throw new \RuntimeException('Could not add ' . $entry . ' to the project package.');
            }
            $zip->addFromString('manifest.json', json_encode($manifest, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) . "\n");
        } finally {
            $zip->close();
        }
        $size = (int)filesize($archive);
        if ($size > ProjectPackageFormat::MAX_ARCHIVE_BYTES) {
            @unlink($archive);
            throw new \RuntimeException('The project package exceeds the ' . ProjectPackageFormat::formatBytes(ProjectPackageFormat::MAX_ARCHIVE_BYTES) . ' archive limit.');
        }
        $slug = ProjectPackageFormat::safeSlug((string)($project['slug'] ?? 'project'));
        return ['path' => $archive, 'filename' => $slug . '-' . date('Ymd-His') . '.' . ProjectPackageFormat::EXTENSION, 'manifest' => $manifest, 'size' => $size];
    }

    private function toBool(mixed $value): bool
    {
        return is_bool($value) ? $value : in_array(strtolower(trim((string)$value)), ['1', 'true', 'yes', 'on'], true);
    }
}
