<?php

declare(strict_types=1);

namespace App\Services\SupportCenter;

use App\Enums\SupportCenter\HostSystemUpdateExecutionStep;
use App\Exceptions\SupportCenter\HostSystemUpdateStagingException;
use App\Models\HostSystemUpdateBackup;
use App\Models\HostSystemUpdateExecution;
use App\Models\HostSystemUpdatePackage;
use Illuminate\Support\Carbon;
use Illuminate\Support\Str;
use ZipArchive;

/**
 * 更新前文件与 .env 快照备份（不替换项目文件）。
 */
final class HostSystemUpdateBackupService
{
    public function __construct(
        private readonly HostSystemUpdateProtectedPathGuard $protectedPathGuard,
        private readonly HostSystemUpdateBackupManifestBuilder $manifestBuilder,
        private readonly HostSystemUpdateExecutionLogger $executionLogger,
        private readonly SupportCenterAuditLogger $auditLogger,
    ) {}

    /**
     * @return list<HostSystemUpdateBackup>
     */
    public function createBackups(HostSystemUpdateExecution $execution): array
    {
        $startedAt = now()->utc();
        $package = $this->resolveReadyPackage($execution);
        $manifest = $this->manifestArray($package);
        $backupDirectory = HostSystemUpdateWorkspaceStorage::backupsDirectory($execution->execution_no);

        HostSystemUpdateWorkspaceStorage::ensureBaseStructureExists();
        HostSystemUpdateWorkspaceStorage::recreateCleanDirectory($backupDirectory);

        $this->auditLogger->logSystemUpdateBackupStarted($execution, HostSystemUpdateBackup::TYPE_FILE_SNAPSHOT);
        $this->executionLogger->logStepStarted(
            $execution,
            HostSystemUpdateExecutionStep::Backup->value,
            (string) __('dberp.system_update.backup.log.started'),
            ['backup_relative_path' => HostSystemUpdateWorkspaceStorage::relativeFromStorage($backupDirectory)],
        );

        try {
            $targetPaths = $this->collectBackupTargetPathsFromManifest($manifest);
            $fileBackup = $this->createFileSnapshotBackup($execution, $targetPaths, $backupDirectory);
            $envBackup = $this->createEnvSnapshotBackup($execution, $backupDirectory);

            $this->executionLogger->logStepCompleted(
                $execution,
                HostSystemUpdateExecutionStep::Backup->value,
                (string) __('dberp.system_update.backup.log.completed'),
                [
                    'file_count' => is_array($fileBackup->included_paths) ? count($fileBackup->included_paths) : 0,
                    'env_snapshot' => $envBackup !== null,
                ],
                $startedAt,
            );

            $this->auditLogger->logSystemUpdateBackupCompleted($execution, $fileBackup);

            $backups = [$fileBackup];

            if ($envBackup instanceof HostSystemUpdateBackup) {
                $backups[] = $envBackup;
            }

            return $backups;
        } catch (HostSystemUpdateStagingException $exception) {
            $this->executionLogger->logStepFailed(
                $execution,
                HostSystemUpdateExecutionStep::Backup->value,
                $exception->userFacingMessage(),
                $exception->errorCode(),
                $exception->userFacingMessage(),
                null,
                $startedAt,
            );
            $this->auditLogger->logSystemUpdateBackupFailed($execution, HostSystemUpdateBackup::TYPE_FILE_SNAPSHOT, $exception);

            throw $exception;
        }
    }

    /**
     * @param  array<string, mixed>  $manifest
     * @return list<string>
     */
    public function collectBackupTargetPaths(array $manifest): array
    {
        return $this->collectBackupTargetPathsFromManifest($manifest);
    }

    /**
     * @param  array<string, mixed>  $manifest
     * @return list<string>
     */
    private function collectBackupTargetPathsFromManifest(array $manifest): array
    {
        $paths = [];

        foreach ([
            HostSystemUpdateManifestContract::FILES => 'files',
            HostSystemUpdateManifestContract::DELETE_FILES => 'delete_files',
        ] as $field => $context) {
            $items = $manifest[$field] ?? [];

            if (! is_array($items)) {
                continue;
            }

            foreach ($items as $path) {
                if (! is_string($path)) {
                    continue;
                }

                $this->protectedPathGuard->assertSafeUpdateTargetPath($path, $context);
                $paths[] = $path;
            }
        }

        return array_values(array_unique($paths));
    }

    /**
     * @param  list<string>  $targetPaths
     */
    private function createFileSnapshotBackup(
        HostSystemUpdateExecution $execution,
        array $targetPaths,
        string $backupDirectory,
    ): HostSystemUpdateBackup {
        $backupNo = $this->generateBackupNo();
        $zipPath = $backupDirectory.DIRECTORY_SEPARATOR.'files.zip';
        $startedAt = now()->utc();

        $backup = new HostSystemUpdateBackup([
            'execution_id' => $execution->getKey(),
            'backup_no' => $backupNo,
            'backup_type' => HostSystemUpdateBackup::TYPE_FILE_SNAPSHOT,
            'status' => HostSystemUpdateBackup::STATUS_RUNNING,
            'started_at' => $startedAt,
        ]);
        $backup->save();

        $backedUpPaths = [];
        $missingPaths = [];
        $fileMeta = [];

        $zip = new ZipArchive;
        $opened = $zip->open($zipPath, ZipArchive::CREATE | ZipArchive::OVERWRITE);

        if ($opened !== true) {
            throw HostSystemUpdateStagingException::backupFailed('zip_create_failed');
        }

        try {
            foreach ($targetPaths as $relativePath) {
                $absolutePath = $this->protectedPathGuard->projectAbsolutePath($relativePath);

                if (! is_file($absolutePath)) {
                    $missingPaths[] = $relativePath;

                    continue;
                }

                $contents = file_get_contents($absolutePath);

                if ($contents === false) {
                    throw HostSystemUpdateStagingException::backupFailed($relativePath);
                }

                $zip->addFromString(str_replace('\\', '/', $relativePath), $contents);
                $backedUpPaths[] = $relativePath;
                $fileMeta[$relativePath] = [
                    'size' => strlen($contents),
                    'checksum' => hash(HostSystemUpdateBackupManifestBuilder::CHECKSUM_ALGORITHM, $contents),
                ];
            }

            $manifest = $this->manifestBuilder->buildFileSnapshotManifest(
                $execution,
                $backedUpPaths,
                $missingPaths,
                $fileMeta,
            );

            $manifestJson = json_encode($manifest, JSON_THROW_ON_ERROR | JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
            $zip->addFromString('backup-manifest.json', $manifestJson);
        } finally {
            $zip->close();
        }

        if (! is_file($zipPath)) {
            throw HostSystemUpdateStagingException::backupFailed('zip_missing');
        }

        $checksum = hash_file(HostSystemUpdateBackupManifestBuilder::CHECKSUM_ALGORITHM, $zipPath);

        $backup->fill([
            'status' => HostSystemUpdateBackup::STATUS_COMPLETED,
            'backup_path' => HostSystemUpdateWorkspaceStorage::relativeFromStorage($zipPath),
            'backup_size' => filesize($zipPath) ?: 0,
            'checksum' => is_string($checksum) ? $checksum : null,
            'checksum_algorithm' => HostSystemUpdateBackupManifestBuilder::CHECKSUM_ALGORITHM,
            'included_paths' => $backedUpPaths,
            'excluded_paths' => $missingPaths,
            'finished_at' => now()->utc(),
        ]);
        $backup->save();

        return $backup->fresh() ?? $backup;
    }

    public function createEnvSnapshotBackup(
        HostSystemUpdateExecution $execution,
        string $backupDirectory,
    ): ?HostSystemUpdateBackup {
        $envPath = base_path('.env');

        if (! is_file($envPath)) {
            return null;
        }

        $backupNo = $this->generateBackupNo();
        $snapshotPath = $backupDirectory.DIRECTORY_SEPARATOR.'env.snapshot';
        $manifestPath = $backupDirectory.DIRECTORY_SEPARATOR.'env-backup-manifest.json';

        if (! copy($envPath, $snapshotPath)) {
            throw HostSystemUpdateStagingException::backupFailed('env_snapshot_copy_failed');
        }

        $checksum = hash_file(HostSystemUpdateBackupManifestBuilder::CHECKSUM_ALGORITHM, $snapshotPath);
        $size = filesize($snapshotPath) ?: 0;
        $manifest = $this->manifestBuilder->buildEnvSnapshotManifest(
            $execution,
            $size,
            is_string($checksum) ? $checksum : '',
        );

        file_put_contents(
            $manifestPath,
            json_encode($manifest, JSON_THROW_ON_ERROR | JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE),
        );

        $backup = new HostSystemUpdateBackup([
            'execution_id' => $execution->getKey(),
            'backup_no' => $backupNo,
            'backup_type' => HostSystemUpdateBackup::TYPE_ENV_SNAPSHOT,
            'status' => HostSystemUpdateBackup::STATUS_COMPLETED,
            'backup_path' => HostSystemUpdateWorkspaceStorage::relativeFromStorage($snapshotPath),
            'backup_size' => $size,
            'checksum' => is_string($checksum) ? $checksum : null,
            'checksum_algorithm' => HostSystemUpdateBackupManifestBuilder::CHECKSUM_ALGORITHM,
            'included_paths' => ['.env'],
            'started_at' => now()->utc(),
            'finished_at' => now()->utc(),
        ]);
        $backup->save();

        $this->auditLogger->logSystemUpdateBackupCompleted($execution, $backup);

        return $backup->fresh();
    }

    private function resolveReadyPackage(HostSystemUpdateExecution $execution): HostSystemUpdatePackage
    {
        $package = $execution->package;

        if (! $package instanceof HostSystemUpdatePackage) {
            throw HostSystemUpdateStagingException::packageNotReady();
        }

        if ($package->manifest_status !== HostSystemUpdatePackage::MANIFEST_STATUS_VALID) {
            throw HostSystemUpdateStagingException::packageNotReady();
        }

        return $package;
    }

    /**
     * @return array<string, mixed>
     */
    private function manifestArray(HostSystemUpdatePackage $package): array
    {
        $manifest = $package->manifest;

        if (! is_array($manifest) || $manifest === []) {
            throw HostSystemUpdateStagingException::packageNotReady();
        }

        return $manifest;
    }

    private function generateBackupNo(): string
    {
        return 'DBUB-'.now()->utc()->format('YmdHis').'-'.strtoupper(Str::random(6));
    }

    /**
     * 分批备份：初始化文件快照 zip 与数据库记录。
     *
     * @param  list<string>  $targetPaths
     */
    public function beginFileSnapshotBackup(
        HostSystemUpdateExecution $execution,
        array $targetPaths,
        string $backupDirectory,
    ): HostSystemUpdateBackup {
        HostSystemUpdateWorkspaceStorage::ensureBaseStructureExists();
        HostSystemUpdateWorkspaceStorage::recreateCleanDirectory($backupDirectory);

        $this->auditLogger->logSystemUpdateBackupStarted($execution, HostSystemUpdateBackup::TYPE_FILE_SNAPSHOT);
        $this->executionLogger->logStepStarted(
            $execution,
            HostSystemUpdateExecutionStep::Backup->value,
            (string) __('dberp.system_update.backup.log.started'),
            ['backup_relative_path' => HostSystemUpdateWorkspaceStorage::relativeFromStorage($backupDirectory)],
        );

        $backupNo = $this->generateBackupNo();
        $zipPath = $backupDirectory.DIRECTORY_SEPARATOR.'files.zip';

        $backup = new HostSystemUpdateBackup([
            'execution_id' => $execution->getKey(),
            'backup_no' => $backupNo,
            'backup_type' => HostSystemUpdateBackup::TYPE_FILE_SNAPSHOT,
            'status' => HostSystemUpdateBackup::STATUS_RUNNING,
            'backup_path' => HostSystemUpdateWorkspaceStorage::relativeFromStorage($zipPath),
            'started_at' => now()->utc(),
        ]);
        $backup->save();

        return $backup->fresh() ?? $backup;
    }

    /**
     * 分批备份：向既有 zip 追加一批文件。
     *
     * @param  list<string>  $targetPaths
     * @return array{backed_up: list<string>, missing: list<string>, file_meta: array<string, array{size: int, checksum: string}>}
     */
    public function appendFileSnapshotBatch(
        string $zipPath,
        array $targetPaths,
    ): array {
        $zip = new ZipArchive;
        $flags = is_file($zipPath) ? 0 : ZipArchive::CREATE;
        $opened = $zip->open($zipPath, $flags);

        if ($opened !== true) {
            throw HostSystemUpdateStagingException::backupFailed('zip_open_failed');
        }

        $backedUp = [];
        $missing = [];
        $fileMeta = [];

        try {
            foreach ($targetPaths as $relativePath) {
                $absolutePath = $this->protectedPathGuard->projectAbsolutePath($relativePath);

                if (! is_file($absolutePath)) {
                    $missing[] = $relativePath;

                    continue;
                }

                $contents = file_get_contents($absolutePath);

                if ($contents === false) {
                    throw HostSystemUpdateStagingException::backupFailed($relativePath);
                }

                $zip->addFromString(str_replace('\\', '/', $relativePath), $contents);
                $backedUp[] = $relativePath;
                $fileMeta[$relativePath] = [
                    'size' => strlen($contents),
                    'checksum' => hash(HostSystemUpdateBackupManifestBuilder::CHECKSUM_ALGORITHM, $contents),
                ];
            }
        } finally {
            $zip->close();
        }

        return [
            'backed_up' => $backedUp,
            'missing' => $missing,
            'file_meta' => $fileMeta,
        ];
    }

    /**
     * 分批备份：写入 manifest 并完成文件快照记录。
     *
     * @param  list<string>  $backedUpPaths
     * @param  list<string>  $missingPaths
     * @param  array<string, array{size: int, checksum: string}>  $fileMeta
     */
    public function finalizeFileSnapshotBackup(
        HostSystemUpdateExecution $execution,
        HostSystemUpdateBackup $backup,
        string $zipPath,
        array $backedUpPaths,
        array $missingPaths,
        array $fileMeta,
        Carbon $startedAt,
    ): HostSystemUpdateBackup {
        $zip = new ZipArchive;
        $opened = $zip->open($zipPath);

        if ($opened !== true) {
            throw HostSystemUpdateStagingException::backupFailed('zip_finalize_failed');
        }

        try {
            $manifest = $this->manifestBuilder->buildFileSnapshotManifest(
                $execution,
                $backedUpPaths,
                $missingPaths,
                $fileMeta,
            );

            $manifestJson = json_encode($manifest, JSON_THROW_ON_ERROR | JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
            $zip->addFromString('backup-manifest.json', $manifestJson);
        } finally {
            $zip->close();
        }

        if (! is_file($zipPath)) {
            throw HostSystemUpdateStagingException::backupFailed('zip_missing');
        }

        $checksum = hash_file(HostSystemUpdateBackupManifestBuilder::CHECKSUM_ALGORITHM, $zipPath);

        $backup->fill([
            'status' => HostSystemUpdateBackup::STATUS_COMPLETED,
            'backup_size' => filesize($zipPath) ?: 0,
            'checksum' => is_string($checksum) ? $checksum : null,
            'checksum_algorithm' => HostSystemUpdateBackupManifestBuilder::CHECKSUM_ALGORITHM,
            'included_paths' => $backedUpPaths,
            'excluded_paths' => $missingPaths,
            'finished_at' => now()->utc(),
        ]);
        $backup->save();

        $completed = $backup->fresh() ?? $backup;

        $this->executionLogger->logStepCompleted(
            $execution,
            HostSystemUpdateExecutionStep::Backup->value,
            (string) __('dberp.system_update.backup.log.completed'),
            [
                'file_count' => count($backedUpPaths),
                'env_snapshot' => false,
            ],
            $startedAt,
        );

        $this->auditLogger->logSystemUpdateBackupCompleted($execution, $completed);

        return $completed;
    }
}
