<?php

declare(strict_types=1);

namespace App\Services\SupportCenter;

use App\Enums\SupportCenter\HostSystemUpdateExecutionStep;
use App\Exceptions\SupportCenter\HostSystemUpdateRecoveryException;
use App\Exceptions\SupportCenter\HostSystemUpdateStagingException;
use App\Models\HostSystemUpdateBackup;
use App\Models\HostSystemUpdateExecution;
use App\Models\HostSystemUpdateExecutionLog;
use ZipArchive;

/**
 * 从 file_snapshot 备份 ZIP 恢复项目文件（不含 .env 全文恢复）。
 */
final class HostSystemUpdateFileRestoreService
{
    public function __construct(
        private readonly HostSystemUpdateProtectedPathGuard $protectedPathGuard,
        private readonly HostSystemUpdateExecutionLogger $executionLogger,
        private readonly SupportCenterAuditLogger $auditLogger,
    ) {}

    /**
     * @return array{restored_count: int, skipped_count: int, missing_count: int, failed_count: int, removed_new_count: int, file_results: list<array<string, string>>}
     */
    public function restoreFromBackup(HostSystemUpdateExecution $execution): array
    {
        $startedAt = now()->utc();
        $backup = $this->resolveFileSnapshotBackup($execution);
        $zipPath = $this->resolveBackupZipPath($backup, $execution);
        $manifest = $this->readBackupManifest($zipPath);

        $this->executionLogger->logStepStarted(
            $execution,
            HostSystemUpdateExecutionStep::Rollback->value,
            (string) __('dberp.system_update.recovery.log.file_restore_started'),
            ['backup_no' => $backup->backup_no],
        );

        $restoredCount = 0;
        $skippedCount = 0;
        $missingCount = 0;
        $failedCount = 0;
        /** @var list<array<string, string>> $fileResults */
        $fileResults = [];

        try {
            $backedUpPaths = $this->normalizePathList($manifest['backed_up_paths'] ?? []);
            $missingAtBackup = $this->normalizePathList($manifest['missing_paths'] ?? []);

            foreach ($backedUpPaths as $relativePath) {
                $result = $this->restoreSinglePath($zipPath, $relativePath);
                $fileResults[] = $result;

                match ($result['status']) {
                    'restored' => $restoredCount++,
                    'missing' => $missingCount++,
                    'skipped' => $skippedCount++,
                    default => $failedCount++,
                };
            }

            $removedNewCount = $this->removeNewlyCreatedFiles($execution, $missingAtBackup);

            if ($failedCount > 0) {
                throw HostSystemUpdateRecoveryException::fileRestoreFailed('partial_restore_failed');
            }

            $summary = [
                'restored_count' => $restoredCount,
                'skipped_count' => $skippedCount,
                'missing_count' => $missingCount,
                'failed_count' => $failedCount,
                'removed_new_count' => $removedNewCount,
                'file_results' => $fileResults,
            ];

            $this->executionLogger->logStepCompleted(
                $execution,
                HostSystemUpdateExecutionStep::Rollback->value,
                (string) __('dberp.system_update.recovery.log.file_restore_completed', [
                    'count' => $restoredCount,
                ]),
                $summary,
                $startedAt,
            );
            $this->auditLogger->logSystemUpdateRollbackFileRestoreCompleted($execution, $summary);

            return $summary;
        } catch (HostSystemUpdateRecoveryException $exception) {
            $this->executionLogger->logStepFailed(
                $execution,
                HostSystemUpdateExecutionStep::Rollback->value,
                $exception->userFacingMessage(),
                $exception->errorCode(),
                $exception->userFacingMessage(),
                [
                    'restored_count' => $restoredCount,
                    'failed_count' => $failedCount,
                ],
                $startedAt,
            );
            $this->auditLogger->logSystemUpdateRollbackFileRestoreFailed($execution, $exception);

            throw $exception;
        }
    }

    public function resolveFileSnapshotBackup(HostSystemUpdateExecution $execution): HostSystemUpdateBackup
    {
        $backup = $execution->backups()
            ->where('backup_type', HostSystemUpdateBackup::TYPE_FILE_SNAPSHOT)
            ->where('status', HostSystemUpdateBackup::STATUS_COMPLETED)
            ->latest('id')
            ->first();

        if (! $backup instanceof HostSystemUpdateBackup) {
            throw HostSystemUpdateRecoveryException::backupUnavailable();
        }

        return $backup;
    }

    private function resolveBackupZipPath(HostSystemUpdateBackup $backup, HostSystemUpdateExecution $execution): string
    {
        $backupDirectory = HostSystemUpdateWorkspaceStorage::backupsDirectory($execution->execution_no);

        $zipPath = $backupDirectory.DIRECTORY_SEPARATOR.'files.zip';

        if (is_file($zipPath)) {
            return $zipPath;
        }

        if (is_string($backup->backup_path) && $backup->backup_path !== '') {
            $resolved = HostSystemUpdateWorkspaceStorage::absoluteFromRelativeStorage($backup->backup_path);

            if (is_file($resolved)) {
                return $resolved;
            }
        }

        throw HostSystemUpdateRecoveryException::backupUnavailable();
    }

    /**
     * @return array<string, mixed>
     */
    private function readBackupManifest(string $zipPath): array
    {
        $zip = new ZipArchive;
        $opened = $zip->open($zipPath);

        if ($opened !== true) {
            throw HostSystemUpdateRecoveryException::fileRestoreFailed('zip_open_failed');
        }

        try {
            $manifestContents = $zip->getFromName('backup-manifest.json');

            if (! is_string($manifestContents) || trim($manifestContents) === '') {
                throw HostSystemUpdateRecoveryException::fileRestoreFailed('backup_manifest_missing');
            }

            $manifest = json_decode($manifestContents, true, 512, JSON_THROW_ON_ERROR);

            if (! is_array($manifest)) {
                throw HostSystemUpdateRecoveryException::fileRestoreFailed('backup_manifest_invalid');
            }

            return $manifest;
        } finally {
            $zip->close();
        }
    }

    /**
     * @return array{path: string, status: string}
     */
    private function restoreSinglePath(string $zipPath, string $relativePath): array
    {
        if ($this->isEnvPath($relativePath)) {
            return ['path' => $relativePath, 'status' => 'skipped'];
        }

        try {
            $this->protectedPathGuard->assertSafeUpdateTargetPath($relativePath, 'rollback');
        } catch (HostSystemUpdateStagingException) {
            return ['path' => $relativePath, 'status' => 'skipped'];
        }

        $normalized = $this->protectedPathGuard->normalizeRelativePath($relativePath);

        if ($normalized === null) {
            throw HostSystemUpdateRecoveryException::fileRestoreFailed($relativePath);
        }

        $zip = new ZipArchive;
        $opened = $zip->open($zipPath);

        if ($opened !== true) {
            throw HostSystemUpdateRecoveryException::fileRestoreFailed('zip_open_failed');
        }

        try {
            $this->protectedPathGuard->assertSafeArchiveEntryName($normalized);
            $contents = $zip->getFromName(str_replace('\\', '/', $normalized));

            if ($contents === false) {
                return ['path' => $normalized, 'status' => 'missing'];
            }
        } catch (HostSystemUpdateStagingException) {
            throw HostSystemUpdateRecoveryException::fileRestoreFailed($normalized);
        } finally {
            $zip->close();
        }

        $targetPath = $this->protectedPathGuard->projectAbsolutePath($normalized);
        $this->copyBytesAtomically($contents, $targetPath);

        return ['path' => $normalized, 'status' => 'restored'];
    }

    /**
     * @param  list<string>  $missingAtBackup
     */
    private function removeNewlyCreatedFiles(HostSystemUpdateExecution $execution, array $missingAtBackup): int
    {
        if ($missingAtBackup === []) {
            return 0;
        }

        $replacedPaths = $this->replacedPathsFromExecutionLogs($execution);
        $removed = 0;

        foreach ($replacedPaths as $path) {
            if (! in_array($path, $missingAtBackup, true)) {
                continue;
            }

            if ($this->isEnvPath($path) || $this->protectedPathGuard->isBlockedPath($path)) {
                continue;
            }

            $absolute = $this->protectedPathGuard->projectAbsolutePath($path);

            if (is_file($absolute) && @unlink($absolute)) {
                $removed++;
            }
        }

        return $removed;
    }

    /**
     * @return list<string>
     */
    private function replacedPathsFromExecutionLogs(HostSystemUpdateExecution $execution): array
    {
        $logs = HostSystemUpdateExecutionLog::query()
            ->where('execution_id', $execution->getKey())
            ->where('step', HostSystemUpdateExecutionStep::ReplaceFiles->value)
            ->orderBy('id')
            ->get();

        $paths = [];

        foreach ($logs as $log) {
            $context = $log->context;

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

            $fileResults = $context['file_results'] ?? null;

            if (is_array($fileResults)) {
                foreach ($fileResults as $result) {
                    if (is_array($result) && ($result['status'] ?? null) === 'replaced' && is_string($result['path'] ?? null)) {
                        $paths[] = $result['path'];
                    }
                }
            }

            if (is_string($context['path'] ?? null) && ($context['status'] ?? null) === 'replaced') {
                $paths[] = $context['path'];
            }
        }

        return array_values(array_unique($paths));
    }

    /**
     * @return list<string>
     */
    private function normalizePathList(mixed $value): array
    {
        if (! is_array($value)) {
            return [];
        }

        $paths = [];

        foreach ($value as $path) {
            if (is_string($path) && trim($path) !== '') {
                $paths[] = str_replace('\\', '/', trim($path));
            }
        }

        return $paths;
    }

    private function isEnvPath(string $path): bool
    {
        $lower = strtolower(str_replace('\\', '/', trim($path)));

        return $lower === '.env' || str_starts_with($lower, '.env.');
    }

    private function copyBytesAtomically(string $contents, string $targetPath): void
    {
        $directory = dirname($targetPath);

        if (! is_dir($directory) && ! mkdir($directory, 0755, true) && ! is_dir($directory)) {
            throw HostSystemUpdateRecoveryException::fileRestoreFailed(basename($targetPath));
        }

        $mode = is_file($targetPath) ? (fileperms($targetPath) & 0777) : 0644;
        $temporaryPath = $targetPath.'.dberp-restore-'.bin2hex(random_bytes(8)).'.tmp';

        if (file_put_contents($temporaryPath, $contents) === false) {
            throw HostSystemUpdateRecoveryException::fileRestoreFailed(basename($targetPath));
        }

        @chmod($temporaryPath, $mode);

        if (! rename($temporaryPath, $targetPath)) {
            @unlink($temporaryPath);

            throw HostSystemUpdateRecoveryException::fileRestoreFailed(basename($targetPath));
        }
    }
}
