<?php

declare(strict_types=1);

namespace ShieldX\Maintenance;

defined('ABSPATH') || exit;

// phpcs:disable WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents,WordPress.WP.AlternativeFunctions.file_system_operations_fopen,WordPress.WP.AlternativeFunctions.file_system_operations_fread,WordPress.WP.AlternativeFunctions.file_system_operations_fwrite,WordPress.WP.AlternativeFunctions.file_system_operations_fclose,WordPress.WP.AlternativeFunctions.file_system_operations_mkdir,WordPress.WP.AlternativeFunctions.file_system_operations_chmod,WordPress.WP.AlternativeFunctions.file_system_operations_tempnam,WordPress.WP.AlternativeFunctions.unlink_unlink,WordPress.WP.AlternativeFunctions.file_system_operations_rmdir,WordPress.WP.AlternativeFunctions.rename_rename -- SHIELD-X manages private backup artifacts and its private remote-storage catalog with atomic same-directory writes and streamed archive extraction.

use ShieldX\Storage\FileStorage;

final class BackupStorageManager
{
    private const CATALOG_FILE = 'google-drive-catalog.json';
    private const MAX_RESTORE_ARCHIVE_ENTRIES = 100000;
    private const MAX_RESTORE_ARCHIVE_UNCOMPRESSED_BYTES = 2147483648;
    private const MAX_RESTORE_ARCHIVE_COMPRESSION_RATIO = 100;

    /** @var FileStorage */
    private $storage;

    /** @var BackupStorageSettings */
    private $settings;

    /** @var GoogleDriveSettings */
    private $googleSettings;

    /** @var GoogleDriveClient */
    private $google;

    /** @var bool */
    private $catalogReadFailed = false;

    public function __construct(FileStorage $storage, ?BackupStorageSettings $settings = null, ?GoogleDriveSettings $googleSettings = null, ?GoogleDriveClient $google = null)
    {
        $this->storage = $storage;
        $this->settings = $settings ?: BackupStorageSettings::fromWordPress();
        $this->googleSettings = $googleSettings ?: GoogleDriveSettings::fromWordPress();
        $this->google = $google ?: new GoogleDriveClient($this->googleSettings);
    }

    public static function fromWordPress(): self
    {
        return new self(FileStorage::fromWordPress());
    }

    /**
     * @return array<string,mixed>
     */
    public function summary(): array
    {
        $catalog = $this->readCatalog();

        return [
            'mode' => $this->settings->mode(),
            'uses_google' => $this->settings->usesGoogle(),
            'keeps_local' => $this->settings->keepsLocal(),
            'google' => $this->googleSettings->summary(),
            'remote_counts' => [
                'database_backups' => count((array) ($catalog['database_backups'] ?? [])),
                'restore_points' => count((array) ($catalog['restore_points'] ?? [])),
            ],
        ];
    }

    /**
     * @return array<string,mixed>
     */
    public function saveMode(string $mode): array
    {
        $mode = BackupStorageSettings::sanitizeMode($mode);
        if (in_array($mode, [BackupStorageSettings::MODE_GOOGLE, BackupStorageSettings::MODE_BOTH], true) && !$this->googleReady()) {
            $this->settings->save(['mode' => BackupStorageSettings::MODE_LOCAL]);

            return [
                'ok' => false,
                'mode' => BackupStorageSettings::MODE_LOCAL,
                'message' => 'Connect Google Drive before selecting Google storage. Local storage remains active.',
            ];
        }

        $this->settings->save(['mode' => $mode]);

        return [
            'ok' => true,
            'mode' => $mode,
            'message' => 'Backup storage mode saved.',
        ];
    }

    /**
     * @return array<string,mixed>
     */
    public function startGoogleAuth(): array
    {
        if (!GoogleDriveSettings::secretStorageReady()) {
            $message = 'OpenSSL and WordPress salts are required before Google Drive can store tokens locally.';
            $this->googleSettings->recordStatus('secret_storage_unavailable', $message);

            return [
                'ok' => false,
                'message' => $message,
            ];
        }

        $result = $this->google->startDeviceAuthorization();
        if (empty($result['ok'])) {
            $this->googleSettings->recordStatus('auth_start_failed', (string) ($result['message'] ?? 'Google authorization failed.'));

            return $result;
        }

        $this->googleSettings->saveDeviceAuthorization($result);

        return $result;
    }

    /**
     * @return array<string,mixed>
     */
    public function completeGoogleAuth(): array
    {
        $deviceCode = $this->googleSettings->deviceCode();
        if ($deviceCode === '') {
            return [
                'ok' => false,
                'message' => 'No pending Google authorization was found.',
            ];
        }

        $result = $this->google->completeDeviceAuthorization($deviceCode);
        if (empty($result['ok'])) {
            $this->googleSettings->recordStatus((string) ($result['status'] ?? 'auth_pending'), (string) ($result['message'] ?? 'Google authorization is not complete.'));

            return $result;
        }

        $refreshToken = (string) ($result['refresh_token'] ?? '');
        if ($refreshToken === '') {
            $message = 'Google did not return a refresh token. Disconnect and authenticate again.';
            $this->googleSettings->recordStatus('missing_refresh_token', $message);

            return [
                'ok' => false,
                'message' => $message,
            ];
        }

        $pending = new GoogleDriveSettings(array_merge($this->googleSettings->all(), [
            'refresh_token' => $refreshToken,
            'status' => GoogleDriveSettings::STATUS_CONNECTED,
        ]));
        $client = new GoogleDriveClient($pending);
        $about = $client->about();
        $root = $client->ensureRootFolder();

        if (empty($about['ok']) || empty($root['ok'])) {
            $message = (string) ($about['message'] ?? $root['message'] ?? 'Google Drive setup could not be verified.');
            $this->googleSettings->recordStatus('connection_verify_failed', $message);

            return [
                'ok' => false,
                'message' => $message,
            ];
        }

        $this->googleSettings->saveConnected($refreshToken, (string) ($about['email'] ?? ''), (string) ($root['id'] ?? ''));

        return [
            'ok' => true,
            'message' => 'Google Drive connected.',
        ];
    }

    /**
     * @return array<string,mixed>
     */
    public function disconnectGoogle(): array
    {
        $revoke = $this->google->revokeRefreshToken();
        $this->googleSettings->disconnect();
        $this->settings->save(['mode' => BackupStorageSettings::MODE_LOCAL]);

        $result = [
            'ok' => true,
            'message' => 'Google Drive disconnected.',
        ];

        if (empty($revoke['ok'])) {
            $result['warning_count'] = 1;
            $result['warning'] = (string) ($revoke['message'] ?? 'Google Drive token revocation failed.');
        }

        return $result;
    }

    /**
     * @return array<string,mixed>
     */
    public function testGoogleConnection(): array
    {
        $result = $this->google->about();
        if (empty($result['ok'])) {
            $this->googleSettings->recordStatus('test_failed', (string) ($result['message'] ?? 'Google Drive connection test failed.'));

            return $result;
        }

        $this->googleSettings->recordStatus('test_ok', '');

        return [
            'ok' => true,
            'message' => 'Google Drive connection works.',
        ];
    }

    /**
     * @param array<string,mixed> $result
     * @return array<string,mixed>
     */
    public function handleDatabaseBackupCreated(array $result): array
    {
        if (empty($result['ok']) || !$this->settings->usesGoogle()) {
            return $this->withStorageState($result, 'local', 'local');
        }

        $backupId = (string) ($result['backup_id'] ?? '');
        $path = (string) ($result['path'] ?? '');
        if ($backupId === '' || $path === '' || !is_file($path)) {
            return $this->withStorageState($result, 'local', 'failed', 'Local database backup was not found for Google upload.');
        }

        if (!$this->googleReady()) {
            $message = $this->googleUnavailableMessage();
            $this->googleSettings->recordStatus('upload_skipped', $message);

            return $this->withStorageState($result, $this->settings->mode(), 'failed', $message);
        }

        if (!$this->catalogReadableForWrite()) {
            $message = 'The Google Drive backup catalog failed integrity verification. Local backup was retained and the remote catalog must be reviewed before uploading more backups.';
            $this->googleSettings->recordStatus('catalog_invalid', $message);

            return $this->withStorageState($result, $this->settings->mode(), 'failed', $message);
        }

        $siteFolder = $this->siteFolderName();
        $folder = $this->google->ensureBackupFolderPath([$siteFolder, 'database-backups']);
        if (empty($folder['ok'])) {
            $message = (string) ($folder['message'] ?? 'Google Drive folder could not be prepared.');
            $this->googleSettings->recordStatus('upload_failed', $message);

            return $this->withStorageState($result, $this->settings->mode(), 'failed', $message);
        }

        $uploadName = basename($path);
        $upload = $this->google->uploadFileResumable($path, $uploadName, (string) ($folder['id'] ?? ''), $this->databaseBackupMimeType($path));
        if (empty($upload['ok'])) {
            $message = (string) ($upload['message'] ?? 'Google Drive upload failed.');
            $this->googleSettings->recordStatus('upload_failed', $message);

            return $this->withStorageState($result, $this->settings->mode(), 'failed', $message);
        }

        $entry = [
            'backup_id' => $backupId,
            'created_at' => (string) ($result['created_at'] ?? $this->createdAtFromBackupFile($path)),
            'table_count' => (int) ($result['tables'] ?? $result['table_count'] ?? 0),
            'rows_exported' => (int) ($result['rows_exported'] ?? 0),
            'warning_count' => max(0, (int) ($result['warning_count'] ?? 0)),
            'warning_details' => $this->sanitizeWarningDetails((array) ($result['warning_details'] ?? [])),
            'size' => (int) ($result['size'] ?? (is_file($path) ? filesize($path) : 0)),
            'md5_checksum' => is_file($path) ? (string) md5_file($path) : '',
            'file_name' => $uploadName,
            'archive_format' => $this->databaseBackupArchiveFormat($path),
            'google_file_id' => (string) ($upload['id'] ?? ''),
            'google_size' => (int) ($upload['size'] ?? 0),
            'google_md5_checksum' => (string) ($upload['md5Checksum'] ?? ''),
            'uploaded_at' => self::now(),
        ];
        $verified = $this->verifyDatabaseBackupGoogleFile($entry);
        if (empty($verified['ok'])) {
            $message = (string) ($verified['message'] ?? 'Google Drive upload could not be verified. Local backup was retained.');
            $this->deleteUploadedGoogleFiles([(string) ($entry['google_file_id'] ?? '')]);
            $this->googleSettings->recordStatus('upload_verify_failed', $message);

            return $this->withStorageState($result, $this->settings->mode(), 'failed', $message);
        }

        if (!$this->updateCatalogLocked(function (array &$catalog) use ($backupId, $entry): bool {
            $catalog['database_backups'][$backupId] = $entry;

            return true;
        })) {
            $message = 'Google Drive upload completed, but SHIELD-X could not save the private remote catalog. Local backup was retained.';
            $this->deleteUploadedGoogleFiles([(string) ($entry['google_file_id'] ?? '')]);
            $this->googleSettings->recordStatus('catalog_write_failed', $message);

            return $this->withStorageState($result, $this->settings->mode(), 'failed', $message);
        }
        $this->googleSettings->recordStatus('upload_ok', '');

        if (!$this->settings->keepsLocal()) {
            $this->deleteLocalFileIfSafe($path, $this->databaseBackupDirectory());
        }

        return $this->withStorageState($result, $this->settings->mode(), 'uploaded');
    }

    /**
     * @param array<string,mixed> $result
     * @return array<string,mixed>
     */
    public function handleRestorePointCreated(array $result): array
    {
        if (empty($result['ok']) || !$this->settings->usesGoogle()) {
            return $this->withStorageState($result, 'local', 'local');
        }

        $restoreId = (string) ($result['restore_id'] ?? '');
        $directory = (string) ($result['path'] ?? '');
        if ($restoreId === '' || $directory === '' || !is_dir($directory)) {
            return $this->withStorageState($result, 'local', 'failed', 'Local restore point was not found for Google upload.');
        }

        if (!$this->googleReady()) {
            $message = $this->googleUnavailableMessage();
            $this->googleSettings->recordStatus('upload_skipped', $message);

            return $this->withStorageState($result, $this->settings->mode(), 'failed', $message);
        }

        if (!$this->catalogReadableForWrite()) {
            $message = 'The Google Drive backup catalog failed integrity verification. Local restore point was retained and the remote catalog must be reviewed before uploading more restore points.';
            $this->googleSettings->recordStatus('catalog_invalid', $message);

            return $this->withStorageState($result, $this->settings->mode(), 'failed', $message);
        }

        $siteFolder = $this->siteFolderName();
        $folder = $this->google->ensureBackupFolderPath([$siteFolder, 'restore-points', $restoreId]);
        if (empty($folder['ok'])) {
            $message = (string) ($folder['message'] ?? 'Google Drive folder could not be prepared.');
            $this->googleSettings->recordStatus('upload_failed', $message);

            return $this->withStorageState($result, $this->settings->mode(), 'failed', $message);
        }

        $upload = $this->uploadRestorePointArchive($directory, $restoreId, (string) ($folder['id'] ?? ''));
        if (empty($upload['ok'])) {
            $message = (string) ($upload['message'] ?? 'Google Drive restore point upload failed.');
            $this->googleSettings->recordStatus('upload_failed', $message);

            return $this->withStorageState($result, $this->settings->mode(), 'failed', $message);
        }

        $databaseFile = '';
        $databaseUpload = [];
        $databaseSize = 0;
        $databaseMd5 = '';
        $databaseBackupId = (string) ($result['database_backup_id'] ?? '');
        if ($databaseBackupId !== '') {
            $databaseFile = $this->localDatabaseBackupPath($databaseBackupId);
            if ($databaseFile !== '') {
                $databaseSize = (int) filesize($databaseFile);
                $databaseMd5 = (string) md5_file($databaseFile);
                $databaseFolder = $this->google->ensureFolder('database', (string) ($folder['id'] ?? ''));
                if (empty($databaseFolder['ok'])) {
                    $message = (string) ($databaseFolder['message'] ?? 'Google Drive database folder could not be prepared.');
                    $this->googleSettings->recordStatus('upload_failed', $message);

                    return $this->withStorageState($result, $this->settings->mode(), 'failed', $message);
                }
                $databaseUpload = $this->google->uploadFileResumable($databaseFile, basename($databaseFile), (string) ($databaseFolder['id'] ?? ''), $this->databaseBackupMimeType($databaseFile));
                if (empty($databaseUpload['ok'])) {
                    $message = (string) ($databaseUpload['message'] ?? 'Google Drive database backup upload failed.');
                    $this->googleSettings->recordStatus('upload_failed', $message);

                    return $this->withStorageState($result, $this->settings->mode(), 'failed', $message);
                }
            }
        }

        $manifest = $this->readJson($directory . '/manifest.json');
        $entry = [
            'restore_id' => $restoreId,
            'created_at' => (string) ($manifest['created_at'] ?? ''),
            'reason' => (string) ($manifest['reason'] ?? ''),
            'files_count' => (int) ($manifest['files_count'] ?? $result['files'] ?? 0),
            'hashes_count' => (int) ($manifest['hashes_count'] ?? 0),
            'indexed_files_count' => (int) ($manifest['indexed_files_count'] ?? 0),
            'bytes_exported' => (int) ($manifest['bytes_exported'] ?? $result['bytes_exported'] ?? 0),
            'snapshot_scope' => (string) ($manifest['snapshot_scope'] ?? ''),
            'snapshot_mode' => (string) ($manifest['snapshot_mode'] ?? 'full'),
            'base_restore_id' => (string) ($manifest['base_restore_id'] ?? ''),
            'database_backup_id' => $databaseBackupId,
            'database_warning_count' => max(0, (int) ($result['database_warning_count'] ?? 0)),
            'database_warning_details' => $this->sanitizeWarningDetails((array) ($result['database_warning_details'] ?? [])),
            'database_google_file_id' => (string) ($databaseUpload['id'] ?? ''),
            'database_file_name' => $databaseFile !== '' ? basename($databaseFile) : '',
            'database_archive_format' => $databaseFile !== '' ? $this->databaseBackupArchiveFormat($databaseFile) : '',
            'database_size' => $databaseSize,
            'database_md5_checksum' => $databaseMd5,
            'database_google_size' => (int) ($databaseUpload['size'] ?? 0),
            'database_google_md5_checksum' => (string) ($databaseUpload['md5Checksum'] ?? ''),
            'truncated' => !empty($manifest['truncated']),
            'google_folder_id' => (string) ($folder['id'] ?? ''),
            'archive_google_file_id' => (string) ($upload['archive_google_file_id'] ?? ''),
            'archive_name' => (string) ($upload['archive_name'] ?? ''),
            'archive_size' => (int) ($upload['archive_size'] ?? 0),
            'archive_md5_checksum' => (string) ($upload['archive_md5_checksum'] ?? ''),
            'archive_google_size' => (int) ($upload['archive_google_size'] ?? 0),
            'archive_google_md5_checksum' => (string) ($upload['archive_google_md5_checksum'] ?? ''),
            'archive_format' => 'zip',
            'files' => [],
            'uploaded_at' => self::now(),
        ];
        $verified = $this->verifyRestorePointGoogleFiles($entry, $databaseBackupId !== '');
        if (empty($verified['ok'])) {
            $message = (string) ($verified['message'] ?? 'Google Drive restore point upload could not be verified. Local restore point was retained.');
            $this->deleteUploadedGoogleFiles([
                (string) ($entry['archive_google_file_id'] ?? ''),
                (string) ($entry['database_google_file_id'] ?? ''),
            ]);
            $this->googleSettings->recordStatus('upload_verify_failed', $message);

            return $this->withStorageState($result, $this->settings->mode(), 'failed', $message);
        }

        if (!$this->updateCatalogLocked(function (array &$catalog) use ($restoreId, $entry): bool {
            $catalog['restore_points'][$restoreId] = $entry;

            return true;
        })) {
            $message = 'Google Drive upload completed, but SHIELD-X could not save the private remote catalog. Local restore point was retained.';
            $this->deleteUploadedGoogleFiles([
                (string) ($entry['archive_google_file_id'] ?? ''),
                (string) ($entry['database_google_file_id'] ?? ''),
            ]);
            $this->googleSettings->recordStatus('catalog_write_failed', $message);

            return $this->withStorageState($result, $this->settings->mode(), 'failed', $message);
        }
        $this->googleSettings->recordStatus('upload_ok', '');

        if (!$this->settings->keepsLocal()) {
            $this->deleteLocalDirectoryIfSafe($directory, $this->restorePointDirectory());
            if ($databaseFile !== '') {
                $this->deleteLocalFileIfSafe($databaseFile, $this->databaseBackupDirectory());
            }
        }

        return $this->withStorageState($result, $this->settings->mode(), 'uploaded');
    }

    public function ensureDatabaseBackupLocal(string $backupId): bool
    {
        $backupId = strtolower(trim($backupId));
        if (!preg_match('/^db-[0-9]{8}-[0-9]{6}-[a-f0-9]{32}$/', $backupId)) {
            return false;
        }

        $existing = $this->localDatabaseBackupPath($backupId);
        if ($existing !== '') {
            return true;
        }

        if (!$this->googleReady()) {
            return false;
        }

        $catalog = $this->readCatalog();
        $entry = isset($catalog['database_backups'][$backupId]) && is_array($catalog['database_backups'][$backupId]) ? $catalog['database_backups'][$backupId] : [];
        $fileId = (string) ($entry['google_file_id'] ?? '');
        if ($fileId === '') {
            return false;
        }

        $target = $this->databaseBackupTargetPath($backupId, $entry);
        $this->ensureDirectory(dirname($target));
        $result = $this->google->downloadFile($fileId, $target);

        return !empty($result['ok']) && is_file($target);
    }

    public function ensureRestorePointLocal(string $restoreId): bool
    {
        $result = $this->ensureRestorePointLocalFromSource($restoreId, 'auto');

        return !empty($result['ok']);
    }

    /**
     * @return array<string,mixed>
     */
    public function ensureRestorePointLocalFromSource(string $restoreId, string $source = 'auto', bool $includeDatabase = true): array
    {
        $restoreId = strtolower(trim($restoreId));
        if (!preg_match('/^restore-[0-9]{8}-[0-9]{6}-[a-f0-9]{32}$/', $restoreId)) {
            return [
                'ok' => false,
                'message' => 'Restore point identifier is invalid.',
            ];
        }

        $directory = $this->restorePointDirectory() . '/' . $restoreId;
        $source = $this->sanitizeRestoreSource($source);
        if ($source === 'local') {
            return is_file($directory . '/manifest.json')
                ? ['ok' => true, 'source' => 'local']
                : ['ok' => false, 'message' => 'Local restore point files are not available.'];
        }

        if ($source === 'auto' && is_file($directory . '/manifest.json')) {
            return [
                'ok' => true,
                'source' => 'local',
            ];
        }

        if (!$this->googleReady()) {
            return [
                'ok' => false,
                'message' => 'Google Drive is not connected. Local restore point files were not changed.',
            ];
        }

        $entry = $this->restorePointCatalogEntry($restoreId);
        $archiveFileId = (string) ($entry['archive_google_file_id'] ?? '');
        $files = isset($entry['files']) && is_array($entry['files']) ? $entry['files'] : [];
        if ($archiveFileId === '' && empty($files)) {
            return [
                'ok' => false,
                'message' => 'No Google Drive catalog entry was found for this restore point.',
            ];
        }

        $available = $this->verifyRestorePointGoogleFiles($entry, $includeDatabase);
        if (empty($available['ok'])) {
            return $available;
        }

        $temporaryToken = $this->temporaryToken();
        if ($temporaryToken === '') {
            return [
                'ok' => false,
                'message' => 'Could not create a private restore temp directory name.',
            ];
        }
        $temporary = $this->restorePointDirectory() . '/.restore-download-' . $restoreId . '-' . $temporaryToken;
        $this->ensureDirectory($temporary);
        if ($archiveFileId !== '') {
            $archiveToken = $this->temporaryToken();
            if ($archiveToken === '') {
                $this->deleteLocalDirectoryIfSafe($temporary, $this->restorePointDirectory());

                return [
                    'ok' => false,
                    'message' => 'Could not create a private restore archive temp name.',
                ];
            }
            $archivePath = $this->restorePointDirectory() . '/.restore-download-' . $restoreId . '-' . $archiveToken . '.zip';
            $download = $this->google->downloadFile($archiveFileId, $archivePath);
            if (empty($download['ok'])) {
                $this->deleteLocalDirectoryIfSafe($temporary, $this->restorePointDirectory());

                return [
                    'ok' => false,
                    'message' => (string) ($download['message'] ?? 'Google Drive restore point archive download failed.'),
                ];
            }

            $extracted = $this->extractRestorePointArchive($archivePath, $temporary);
            $this->deleteFile($archivePath);
            if (empty($extracted['ok'])) {
                $this->deleteLocalDirectoryIfSafe($temporary, $this->restorePointDirectory());

                return $extracted;
            }
        } else {
            foreach ($files as $file) {
                if (!is_array($file)) {
                    continue;
                }

                $relative = $this->cleanRelativePath((string) ($file['relative_path'] ?? ''));
                $fileId = (string) ($file['google_file_id'] ?? '');
                if ($relative === '' || $fileId === '') {
                    continue;
                }

                $target = $temporary . '/' . $relative;
                $result = $this->google->downloadFile($fileId, $target);
                if (empty($result['ok'])) {
                    $this->deleteLocalDirectoryIfSafe($temporary, $this->restorePointDirectory());

                    return [
                        'ok' => false,
                        'message' => (string) ($result['message'] ?? 'Google Drive restore point download failed.'),
                    ];
                }
            }
        }

        if (!is_file($temporary . '/manifest.json')) {
            $this->deleteLocalDirectoryIfSafe($temporary, $this->restorePointDirectory());

            return [
                'ok' => false,
                'message' => 'Google Drive restore point download did not include a manifest.',
            ];
        }

        $databaseTemporary = '';
        $databaseBackupId = (string) ($entry['database_backup_id'] ?? '');
        $databaseFileId = (string) ($entry['database_google_file_id'] ?? '');
        if ($includeDatabase && $databaseBackupId !== '' && $databaseFileId !== '') {
            $this->ensureDirectory($this->databaseBackupDirectory());
            $databaseToken = $this->temporaryToken();
            if ($databaseToken === '') {
                $this->deleteLocalDirectoryIfSafe($temporary, $this->restorePointDirectory());

                return [
                    'ok' => false,
                    'message' => 'Could not create a private database restore temp name.',
                ];
            }
            $databaseExtension = $this->databaseBackupExtensionFromEntry($entry, 'database_');
            $databaseTemporary = $this->databaseBackupDirectory() . '/.google-restore-' . $databaseBackupId . '-' . $databaseToken . '.' . $databaseExtension;
            $download = $this->google->downloadFile($databaseFileId, $databaseTemporary);
            if (empty($download['ok'])) {
                $this->deleteLocalDirectoryIfSafe($temporary, $this->restorePointDirectory());
                $this->deleteFile($databaseTemporary);

                return [
                    'ok' => false,
                    'message' => (string) ($download['message'] ?? 'Google Drive database backup download failed.'),
                ];
            }
        }

        if (is_dir($directory)) {
            $this->deleteLocalDirectoryIfSafe($directory, $this->restorePointDirectory());
        }
        $this->ensureDirectory(dirname($directory));
        if (!rename($temporary, $directory)) {
            $this->deleteLocalDirectoryIfSafe($temporary, $this->restorePointDirectory());
            $this->deleteFile($databaseTemporary);

            return [
                'ok' => false,
                'message' => 'Could not stage the Google Drive restore point locally.',
            ];
        }

        if ($databaseTemporary !== '') {
            $databaseTarget = $this->databaseBackupTargetPath($databaseBackupId, $entry, 'database_');
            $this->deleteLocalFileIfSafe($databaseTarget, $this->databaseBackupDirectory());
            $databaseAlternate = $this->alternateDatabaseBackupPath($databaseBackupId, $databaseTarget);
            if ($databaseAlternate !== '') {
                $this->deleteLocalFileIfSafe($databaseAlternate, $this->databaseBackupDirectory());
            }
            if (!rename($databaseTemporary, $databaseTarget)) {
                $this->deleteFile($databaseTemporary);

                return [
                    'ok' => false,
                    'message' => 'Could not stage the Google Drive database backup locally.',
                ];
            }
            chmod($databaseTarget, 0600);
        }

        return [
            'ok' => true,
            'source' => 'google',
        ];
    }

    /**
     * @return array<string,mixed>
     */
    public function restorePointStorageState(string $restoreId): array
    {
        $restoreId = strtolower(trim($restoreId));
        if (!preg_match('/^restore-[0-9]{8}-[0-9]{6}-[a-f0-9]{32}$/', $restoreId)) {
            return [
                'local_exists' => false,
                'google_cataloged' => false,
                'google_connected' => $this->googleReady(),
            ];
        }

        $entry = $this->restorePointCatalogEntry($restoreId);

        return [
            'local_exists' => is_file($this->restorePointDirectory() . '/' . $restoreId . '/manifest.json'),
            'google_cataloged' => ((string) ($entry['archive_google_file_id'] ?? '') !== '') || (!empty($entry['files']) && is_array($entry['files'])),
            'google_connected' => $this->googleReady(),
        ];
    }

    /**
     * @param array<int,array<string,mixed>> $items
     * @return array<int,array<string,mixed>>
     */
    public function mergeDatabaseBackups(array $items, int $limit = 8): array
    {
        $byId = [];
        foreach ($items as $item) {
            if (!is_array($item)) {
                continue;
            }

            $id = (string) ($item['backup_id'] ?? '');
            if ($id === '') {
                continue;
            }

            $item['storage_provider'] = 'local';
            $item['remote_status'] = isset($item['remote_status']) ? (string) $item['remote_status'] : 'local';
            $item['local_exists'] = true;
            $byId[$id] = $item;
        }

        $catalog = $this->readCatalog();
        foreach ((array) ($catalog['database_backups'] ?? []) as $id => $entry) {
            if (!is_array($entry)) {
                continue;
            }

            $id = (string) ($entry['backup_id'] ?? $id);
            if ($id === '') {
                continue;
            }

            if (isset($byId[$id])) {
                $byId[$id]['storage_provider'] = 'both';
                $byId[$id]['remote_status'] = 'uploaded';
                continue;
            }

            $byId[$id] = [
                'backup_id' => $id,
                'created_at' => (string) ($entry['created_at'] ?? ''),
                'table_count' => (int) ($entry['table_count'] ?? 0),
                'rows_exported' => (int) ($entry['rows_exported'] ?? 0),
                'warning_count' => max(0, (int) ($entry['warning_count'] ?? 0)),
                'warning_details' => $this->sanitizeWarningDetails((array) ($entry['warning_details'] ?? [])),
                'size' => (int) ($entry['size'] ?? 0),
                'metadata_complete' => true,
                'storage_provider' => 'google',
                'remote_status' => 'uploaded',
                'local_exists' => false,
            ];
        }

        return $this->sortAndLimit(array_values($byId), $limit);
    }

    /**
     * @param array<int,array<string,mixed>> $items
     * @return array<int,array<string,mixed>>
     */
    public function mergeRestorePoints(array $items, int $limit = 8): array
    {
        $byId = [];
        foreach ($items as $item) {
            if (!is_array($item)) {
                continue;
            }

            $id = (string) ($item['restore_id'] ?? '');
            if ($id === '') {
                continue;
            }

            $item['storage_provider'] = 'local';
            $item['remote_status'] = isset($item['remote_status']) ? (string) $item['remote_status'] : 'local';
            $item['local_exists'] = true;
            $byId[$id] = $item;
        }

        $catalog = $this->readCatalog();
        foreach ((array) ($catalog['restore_points'] ?? []) as $id => $entry) {
            if (!is_array($entry)) {
                continue;
            }

            $id = (string) ($entry['restore_id'] ?? $id);
            if ($id === '') {
                continue;
            }

            if (isset($byId[$id])) {
                $byId[$id]['storage_provider'] = 'both';
                $byId[$id]['remote_status'] = 'uploaded';
                continue;
            }

            $entry['storage_provider'] = 'google';
            $entry['remote_status'] = 'uploaded';
            $entry['local_exists'] = false;
            $byId[$id] = $entry;
        }

        return $this->sortAndLimit(array_values($byId), $limit);
    }

    /**
     * @return array<string,mixed>
     */
    private function restorePointCatalogEntry(string $restoreId): array
    {
        $catalog = $this->readCatalog();
        $entry = isset($catalog['restore_points'][$restoreId]) && is_array($catalog['restore_points'][$restoreId]) ? $catalog['restore_points'][$restoreId] : [];

        return $entry;
    }

    /**
     * @param array<string,mixed> $entry
     * @return array<string,mixed>
     */
    private function verifyRestorePointGoogleFiles(array $entry, bool $includeDatabase): array
    {
        $archiveFileId = (string) ($entry['archive_google_file_id'] ?? '');
        if ($archiveFileId !== '') {
            $available = $this->google->fileAvailable($archiveFileId);
            if (empty($available['ok'])) {
                return [
                    'ok' => false,
                    'message' => (string) ($available['message'] ?? 'The Google Drive restore point archive is not available.'),
                ];
            }

            $verified = $this->verifyGoogleFileMetadata($entry, $available, 'archive_size', 'archive_md5_checksum');
            if (empty($verified['ok'])) {
                return $verified;
            }

            return $this->verifyRestorePointGoogleDatabaseFile($entry, $includeDatabase);
        }

        $files = isset($entry['files']) && is_array($entry['files']) ? $entry['files'] : [];
        if (empty($files)) {
            return [
                'ok' => false,
                'message' => 'No Google Drive files were recorded for this restore point.',
            ];
        }

        foreach ($files as $file) {
            if (!is_array($file)) {
                continue;
            }

            $fileId = (string) ($file['google_file_id'] ?? '');
            if ($fileId === '') {
                return [
                    'ok' => false,
                    'message' => 'A Google Drive restore point file is missing its file identifier.',
                ];
            }

            $available = $this->google->fileAvailable($fileId);
            if (empty($available['ok'])) {
                return [
                    'ok' => false,
                    'message' => (string) ($available['message'] ?? 'A Google Drive restore point file is not available.'),
                ];
            }
        }

        return $this->verifyRestorePointGoogleDatabaseFile($entry, $includeDatabase);
    }

    /**
     * @param array<string,mixed> $entry
     * @return array<string,mixed>
     */
    private function verifyDatabaseBackupGoogleFile(array $entry): array
    {
        $fileId = (string) ($entry['google_file_id'] ?? '');
        if ($fileId === '') {
            return [
                'ok' => false,
                'message' => 'Google Drive database backup file was not recorded.',
            ];
        }

        $available = $this->google->fileAvailable($fileId);
        if (empty($available['ok'])) {
            return [
                'ok' => false,
                'message' => (string) ($available['message'] ?? 'The Google Drive database backup is not available.'),
            ];
        }

        return $this->verifyGoogleFileMetadata($entry, $available, 'size', 'md5_checksum');
    }

    /**
     * @param array<string,mixed> $entry
     * @return array<string,mixed>
     */
    private function verifyRestorePointGoogleDatabaseFile(array $entry, bool $includeDatabase): array
    {
        $databaseBackupId = (string) ($entry['database_backup_id'] ?? '');
        $databaseFileId = (string) ($entry['database_google_file_id'] ?? '');
        if ($includeDatabase && $databaseBackupId !== '' && $databaseFileId !== '') {
            $available = $this->google->fileAvailable($databaseFileId);
            if (empty($available['ok'])) {
                return [
                    'ok' => false,
                    'message' => (string) ($available['message'] ?? 'The Google Drive database backup is not available.'),
                ];
            }

            $verified = $this->verifyGoogleFileMetadata($entry, $available, 'database_size', 'database_md5_checksum');
            if (empty($verified['ok'])) {
                return $verified;
            }
        }

        return ['ok' => true];
    }

    /**
     * @param array<string,mixed> $entry
     * @param array<string,mixed> $available
     * @return array<string,mixed>
     */
    private function verifyGoogleFileMetadata(array $entry, array $available, string $sizeKey, string $md5Key): array
    {
        $expectedSize = (int) ($entry[$sizeKey] ?? 0);
        $actualSize = (int) ($available['size'] ?? 0);
        if ($expectedSize > 0 && $actualSize !== $expectedSize) {
            return [
                'ok' => false,
                'message' => 'Google Drive backup verification failed because the remote file size does not match the local backup.',
            ];
        }

        $expectedMd5 = strtolower((string) ($entry[$md5Key] ?? ''));
        $actualMd5 = strtolower((string) ($available['md5Checksum'] ?? ''));
        if ($expectedMd5 !== '' && $actualMd5 !== '' && !hash_equals($expectedMd5, $actualMd5)) {
            return [
                'ok' => false,
                'message' => 'Google Drive backup verification failed because the remote file checksum does not match the local backup.',
            ];
        }

        return ['ok' => true];
    }

    /**
     * @param array<int,string> $fileIds
     */
    private function deleteUploadedGoogleFiles(array $fileIds): void
    {
        $seen = [];
        foreach ($fileIds as $fileId) {
            $fileId = trim((string) $fileId);
            if ($fileId === '' || isset($seen[$fileId])) {
                continue;
            }

            $seen[$fileId] = true;
            try {
                $this->google->deleteFile($fileId);
            } catch (\Throwable $error) {
                continue;
            }
        }
    }

    private function sanitizeRestoreSource(string $source): string
    {
        $source = strtolower(trim($source));

        return in_array($source, ['auto', 'local', 'google'], true) ? $source : 'auto';
    }

    private function googleReady(): bool
    {
        return $this->googleSettings->credentialsAvailable()
            && GoogleDriveSettings::secretStorageReady()
            && $this->googleSettings->connected();
    }

    private function googleUnavailableMessage(): string
    {
        if (!$this->googleSettings->credentialsAvailable()) {
            return 'Google Drive sign-in is not configured yet. Local backup was retained.';
        }

        if (!GoogleDriveSettings::secretStorageReady()) {
            return 'OpenSSL and WordPress salts are required before Google Drive can store tokens locally.';
        }

        return 'Google Drive is not connected. Local backup was retained.';
    }

    /**
     * @return array<string,mixed>
     */
    private function uploadRestorePointArchive(string $directory, string $restoreId, string $remoteFolderId): array
    {
        $archive = $this->createRestorePointArchive($directory, $restoreId);
        if (empty($archive['ok'])) {
            return $archive;
        }

        $path = (string) ($archive['path'] ?? '');
        $name = (string) ($archive['name'] ?? ($restoreId . '.zip'));
        $size = is_file($path) ? (int) filesize($path) : 0;
        $md5 = is_file($path) ? (string) md5_file($path) : '';
        $upload = $this->google->uploadFileResumable($path, $name, $remoteFolderId, 'application/zip');
        $this->deleteFile($path);
        if (empty($upload['ok'])) {
            return $upload;
        }

        return [
            'ok' => true,
            'archive_google_file_id' => (string) ($upload['id'] ?? ''),
            'archive_name' => $name,
            'archive_size' => $size,
            'archive_md5_checksum' => $md5,
            'archive_google_size' => (int) ($upload['size'] ?? 0),
            'archive_google_md5_checksum' => (string) ($upload['md5Checksum'] ?? ''),
        ];
    }

    /**
     * @return array<string,mixed>
     */
    private function createRestorePointArchive(string $directory, string $restoreId): array
    {
        if (!class_exists('\ZipArchive')) {
            return [
                'ok' => false,
                'message' => 'PHP ZipArchive is required before SHIELD-X can store restore points as Google Drive archives.',
            ];
        }

        $files = $this->restorePointFiles($directory);
        if (empty($files) || !isset($files['manifest.json'])) {
            return [
                'ok' => false,
                'message' => 'Local restore point files were not found for Google archive upload.',
            ];
        }

        $archiveDirectory = $this->storage->baseDir() . '/tmp/google-restore-archives';
        $this->ensureDirectory($archiveDirectory);
        $archiveToken = $this->temporaryToken();
        if ($archiveToken === '') {
            return [
                'ok' => false,
                'message' => 'Could not create a private restore point archive name.',
            ];
        }
        $archivePath = $archiveDirectory . '/' . $restoreId . '-' . $archiveToken . '.zip';
        $zip = new \ZipArchive();
        if ($zip->open($archivePath, \ZipArchive::CREATE | \ZipArchive::OVERWRITE) !== true) {
            return [
                'ok' => false,
                'message' => 'Could not create the restore point archive for Google Drive.',
            ];
        }

        foreach ($files as $relative => $path) {
            $relative = $this->cleanRelativePath($relative);
            if ($relative === '') {
                continue;
            }
            if (!$zip->addFile($path, $relative)) {
                $zip->close();
                $this->deleteFile($archivePath);

                return [
                    'ok' => false,
                    'message' => 'Could not add restore point files to the Google Drive archive.',
                ];
            }
        }

        if (!$zip->close()) {
            $this->deleteFile($archivePath);

            return [
                'ok' => false,
                'message' => 'Could not finalize the restore point archive for Google Drive.',
            ];
        }
        chmod($archivePath, 0600);

        return [
            'ok' => true,
            'path' => $archivePath,
            'name' => $restoreId . '.zip',
        ];
    }

    /**
     * @return array<string,mixed>
     */
    private function extractRestorePointArchive(string $archivePath, string $targetDirectory): array
    {
        if (!class_exists('\ZipArchive')) {
            return [
                'ok' => false,
                'message' => 'PHP ZipArchive is required before SHIELD-X can restore Google Drive archive backups.',
            ];
        }

        if (!is_file($archivePath) || !is_readable($archivePath)) {
            return [
                'ok' => false,
                'message' => 'Google Drive restore point archive was not downloaded.',
            ];
        }

        $zip = new \ZipArchive();
        if ($zip->open($archivePath) !== true) {
            return [
                'ok' => false,
                'message' => 'Google Drive restore point archive could not be opened.',
            ];
        }

        $validationError = $this->restorePointArchiveValidationError($zip);
        if ($validationError !== '') {
            $zip->close();

            return [
                'ok' => false,
                'message' => $validationError,
            ];
        }

        for ($index = 0; $index < $zip->numFiles; $index++) {
            $name = $zip->getNameIndex($index);
            $isDirectory = is_string($name) && substr($name, -1) === '/';
            $relative = is_string($name) ? $this->cleanRelativePath($name) : '';
            if ($relative === '') {
                $zip->close();

                return [
                    'ok' => false,
                    'message' => 'Google Drive restore point archive contains an unsafe path.',
                ];
            }

            if ($isDirectory) {
                $this->ensureDirectory($targetDirectory . '/' . $relative);
                continue;
            }

            $source = $zip->getStream($name);
            if (!is_resource($source)) {
                $zip->close();

                return [
                    'ok' => false,
                    'message' => 'Google Drive restore point archive could not be extracted.',
                ];
            }

            $target = $targetDirectory . '/' . $relative;
            $this->ensureDirectory(dirname($target));
            $destination = fopen($target, 'wb');
            if (!is_resource($destination)) {
                fclose($source);
                $zip->close();

                return [
                    'ok' => false,
                    'message' => 'Could not write extracted Google Drive restore point files.',
                ];
            }

            while (!feof($source)) {
                $chunk = fread($source, 1048576);
                if (!is_string($chunk)) {
                    fclose($source);
                    fclose($destination);
                    $zip->close();

                    return [
                        'ok' => false,
                        'message' => 'Google Drive restore point archive could not be extracted.',
                    ];
                }

                if ($chunk !== '' && fwrite($destination, $chunk) === false) {
                    fclose($source);
                    fclose($destination);
                    $zip->close();

                    return [
                        'ok' => false,
                        'message' => 'Could not write extracted Google Drive restore point files.',
                    ];
                }
            }
            fclose($source);
            fclose($destination);
            chmod($target, 0600);
        }

        $zip->close();

        return ['ok' => true];
    }

    private function restorePointArchiveValidationError(\ZipArchive $zip): string
    {
        $entries = (int) $zip->numFiles;
        if ($entries < 0 || $entries > self::MAX_RESTORE_ARCHIVE_ENTRIES) {
            return 'Google Drive restore point archive contains too many files.';
        }

        $uncompressed = 0;
        $compressed = 0;
        for ($index = 0; $index < $entries; $index++) {
            $name = $zip->getNameIndex($index);
            $relative = is_string($name) ? $this->cleanRelativePath($name) : '';
            if ($relative === '') {
                return 'Google Drive restore point archive contains an unsafe path.';
            }

            if (is_string($name) && substr($name, -1) === '/') {
                continue;
            }

            $stat = $zip->statIndex($index);
            $size = is_array($stat) && isset($stat['size']) && is_numeric($stat['size']) ? (int) $stat['size'] : -1;
            $compressedSize = is_array($stat) && isset($stat['comp_size']) && is_numeric($stat['comp_size']) ? (int) $stat['comp_size'] : -1;
            if ($size < 0 || $compressedSize < 0 || $size > self::MAX_RESTORE_ARCHIVE_UNCOMPRESSED_BYTES - $uncompressed) {
                return 'Google Drive restore point archive exceeds the extraction size limit.';
            }

            $uncompressed += $size;
            $compressed = $compressedSize > self::MAX_RESTORE_ARCHIVE_UNCOMPRESSED_BYTES - $compressed
                ? self::MAX_RESTORE_ARCHIVE_UNCOMPRESSED_BYTES
                : $compressed + $compressedSize;
            $limitError = self::restorePointArchiveLimitError($entries, $uncompressed, $compressed);
            if ($limitError !== '') {
                return $limitError;
            }
        }

        return '';
    }

    private static function restorePointArchiveLimitError(int $entries, int $uncompressed, int $compressed): string
    {
        if ($entries < 0 || $entries > self::MAX_RESTORE_ARCHIVE_ENTRIES) {
            return 'Google Drive restore point archive contains too many files.';
        }

        if ($uncompressed < 0 || $uncompressed > self::MAX_RESTORE_ARCHIVE_UNCOMPRESSED_BYTES) {
            return 'Google Drive restore point archive exceeds the extraction size limit.';
        }

        if (
            $uncompressed > 0
            && ($compressed <= 0 || intdiv($uncompressed - 1, self::MAX_RESTORE_ARCHIVE_COMPRESSION_RATIO) >= $compressed)
        ) {
            return 'Google Drive restore point archive exceeds the allowed compression ratio.';
        }

        return '';
    }

    /**
     * @param array<string,string> $folderCache
     */
    private function ensureRemoteRelativeFolder(string $relative, array &$folderCache): string
    {
        $relative = trim(str_replace('\\', '/', $relative), '/');
        if (isset($folderCache[$relative])) {
            return $folderCache[$relative];
        }

        $parts = $relative === '' ? [] : explode('/', $relative);
        $path = '';
        foreach ($parts as $part) {
            $parent = $path;
            $path = $path === '' ? $part : $path . '/' . $part;
            if (isset($folderCache[$path])) {
                continue;
            }

            $folder = $this->google->ensureFolder($part, $folderCache[$parent] ?? '');
            if (empty($folder['ok'])) {
                return '';
            }

            $folderCache[$path] = (string) ($folder['id'] ?? '');
        }

        return $folderCache[$relative] ?? '';
    }

    /**
     * @return array<string,string>
     */
    private function restorePointFiles(string $directory): array
    {
        $directory = rtrim(str_replace('\\', '/', $directory), '/');
        $items = [];
        if (!is_dir($directory)) {
            return $items;
        }

        $iterator = new \RecursiveIteratorIterator(
            new \RecursiveDirectoryIterator($directory, \FilesystemIterator::SKIP_DOTS),
            \RecursiveIteratorIterator::SELF_FIRST
        );

        foreach ($iterator as $item) {
            if (!$item instanceof \SplFileInfo || !$item->isFile()) {
                continue;
            }

            $path = str_replace('\\', '/', $item->getPathname());
            $relative = ltrim(substr($path, strlen($directory)), '/');
            if ($relative !== '') {
                $items[$relative] = $path;
            }
        }

        ksort($items);

        return $items;
    }

    /**
     * @param array<string,mixed> $result
     * @return array<string,mixed>
     */
    private function withStorageState(array $result, string $provider, string $status, string $message = ''): array
    {
        $result['storage_provider'] = $provider;
        $result['remote_status'] = $status;
        $result['local_exists'] = !empty($result['path']) && (is_file((string) $result['path']) || is_dir((string) $result['path']));
        if ($message !== '') {
            $result['storage_message'] = $message;
            $result['warning_count'] = max(0, (int) ($result['warning_count'] ?? 0)) + 1;
        }

        return $result;
    }

    /**
     * @param array<int,array<string,mixed>> $items
     * @return array<int,array<string,mixed>>
     */
    private function sortAndLimit(array $items, int $limit): array
    {
        usort($items, static function (array $left, array $right): int {
            return strtotime((string) ($right['created_at'] ?? '')) <=> strtotime((string) ($left['created_at'] ?? ''));
        });

        return array_slice($items, 0, max(1, min(50, $limit)));
    }

    /**
     * @param callable $updater Receives the catalog array by reference and returns whether it changed safely.
     */
    private function updateCatalogLocked(callable $updater): bool
    {
        return $this->withCatalogLock(function () use ($updater): bool {
            $this->catalogReadFailed = false;
            $catalog = $this->readCatalog();
            if ($this->catalogReadFailed) {
                return false;
            }

            if (!$updater($catalog)) {
                return false;
            }

            return $this->writeCatalog($catalog);
        });
    }

    private function catalogReadableForWrite(): bool
    {
        return $this->withCatalogLock(function (): bool {
            $this->catalogReadFailed = false;
            $this->readCatalog();

            return !$this->catalogReadFailed;
        });
    }

    /**
     * @return bool
     */
    private function withCatalogLock(callable $callback): bool
    {
        $this->storage->ensure();
        $directory = dirname($this->catalogPath());
        if (!is_dir($directory)) {
            if (function_exists('wp_mkdir_p')) {
                wp_mkdir_p($directory);
            } else {
                mkdir($directory, 0755, true);
            }
        }

        $lockPath = $this->catalogPath() . '.lock';
        $handle = fopen($lockPath, 'c');
        if (!is_resource($handle)) {
            return (bool) $callback();
        }

        try {
            if (!flock($handle, LOCK_EX)) {
                return false;
            }

            return (bool) $callback();
        } finally {
            flock($handle, LOCK_UN);
            fclose($handle);
        }
    }

    /**
     * @return array<string,mixed>
     */
    private function readCatalog(): array
    {
        $path = $this->catalogPath();
        if (!is_file($path) || !is_readable($path)) {
            return $this->emptyCatalog();
        }

        $raw = file_get_contents($path);
        $decoded = is_string($raw) ? \ShieldX\Support\Json::decodeArrayRaw($raw) : null;
        if (!is_array($decoded)) {
            if (is_string($raw) && trim($raw) !== '') {
                $this->catalogReadFailed = true;
                $this->quarantineCatalog($path);
            }

            return $this->emptyCatalog();
        }

        $hmac = isset($decoded['catalog_hmac']) && is_scalar($decoded['catalog_hmac']) ? (string) $decoded['catalog_hmac'] : '';
        if ($hmac === '' || !hash_equals($hmac, $this->catalogHmac($decoded))) {
            $this->catalogReadFailed = true;
            $this->quarantineCatalog($path);

            return $this->emptyCatalog();
        }

        $decoded['database_backups'] = isset($decoded['database_backups']) && is_array($decoded['database_backups']) ? $decoded['database_backups'] : [];
        $decoded['restore_points'] = isset($decoded['restore_points']) && is_array($decoded['restore_points']) ? $decoded['restore_points'] : [];

        return $decoded;
    }

    private function quarantineCatalog(string $path): void
    {
        if (!is_file($path)) {
            return;
        }

        $directory = dirname($path);
        $quarantine = $directory . '/corrupt-' . gmdate('Ymd-His') . '-' . basename($path);
        if (is_file($quarantine)) {
            $quarantine .= '-' . bin2hex(random_bytes(4));
        }

        // phpcs:ignore WordPress.WP.AlternativeFunctions.rename_rename -- Preserve the damaged private catalog for manual recovery instead of overwriting it.
        if (@rename($path, $quarantine) || (@copy($path, $quarantine) && @unlink($path))) {
            @chmod($quarantine, 0600);
        }
    }

    /**
     * @param array<string,mixed> $catalog
     */
    private function writeCatalog(array $catalog): bool
    {
        if ($this->catalogReadFailed) {
            return false;
        }

        $this->storage->ensure();
        $directory = dirname($this->catalogPath());
        if (!is_dir($directory)) {
            if (function_exists('wp_mkdir_p')) {
                wp_mkdir_p($directory);
            } else {
                mkdir($directory, 0755, true);
            }
        }

        $catalog['updated_at'] = self::now();
        $catalog['catalog_hmac'] = $this->catalogHmac($catalog);
        $json = wp_json_encode($catalog, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
        if (!is_string($json)) {
            return false;
        }

        $temporary = tempnam($directory, 'shieldx-remote-');
        if (!is_string($temporary) || $temporary === '') {
            return false;
        }

        if (file_put_contents($temporary, $json . "\n", LOCK_EX) === false) {
            $this->deleteFile($temporary);
            return false;
        }

        chmod($temporary, 0600);
        if (!rename($temporary, $this->catalogPath())) {
            $this->deleteFile($temporary);
            return false;
        }
        chmod($this->catalogPath(), 0600);

        return true;
    }

    /**
     * @return array<string,mixed>
     */
    private function emptyCatalog(): array
    {
        return [
            'version' => 1,
            'provider' => 'google_drive',
            'database_backups' => [],
            'restore_points' => [],
            'updated_at' => '',
            'catalog_hmac' => '',
        ];
    }

    /**
     * @param array<string,mixed> $catalog
     */
    private function catalogHmac(array $catalog): string
    {
        unset($catalog['catalog_hmac']);
        $json = wp_json_encode($this->canonicalizeForHmac($catalog), JSON_UNESCAPED_SLASHES);

        return hash_hmac('sha256', is_string($json) ? $json : '', $this->catalogHmacKey());
    }

    /**
     * @param mixed $value
     * @return mixed
     */
    private function canonicalizeForHmac($value)
    {
        if (!is_array($value)) {
            return $value;
        }

        $isList = array_keys($value) === range(0, count($value) - 1);
        if (!$isList) {
            ksort($value);
        }

        foreach ($value as $key => $item) {
            $value[$key] = $this->canonicalizeForHmac($item);
        }

        return $value;
    }

    private function catalogHmacKey(): string
    {
        $auth = function_exists('wp_salt') ? (string) wp_salt('auth') : '';
        $secure = function_exists('wp_salt') ? (string) wp_salt('secure_auth') : '';
        $material = $auth . '|' . $secure;
        if ($material === '|') {
            $material = __FILE__;
        }

        return hash('sha256', $material . '|shieldx-google-drive-catalog-hmac', true);
    }

    private function catalogPath(): string
    {
        return $this->storage->baseDir() . '/remote-storage/' . self::CATALOG_FILE;
    }

    private function databaseBackupDirectory(): string
    {
        return $this->storage->baseDir() . '/db-backups';
    }

    private function localDatabaseBackupPath(string $backupId): string
    {
        $backupId = strtolower(trim($backupId));
        if (!preg_match('/^db-[0-9]{8}-[0-9]{6}-[a-f0-9]{32}$/', $backupId)) {
            return '';
        }

        foreach (['zip', 'json'] as $extension) {
            $path = $this->databaseBackupDirectory() . '/' . $backupId . '.' . $extension;
            if (is_file($path)) {
                return $path;
            }
        }

        return '';
    }

    /**
     * @param array<string,mixed> $entry
     */
    private function databaseBackupTargetPath(string $backupId, array $entry = [], string $prefix = ''): string
    {
        $backupId = strtolower(trim($backupId));
        if (!preg_match('/^db-[0-9]{8}-[0-9]{6}-[a-f0-9]{32}$/', $backupId)) {
            return '';
        }

        return $this->databaseBackupDirectory() . '/' . $backupId . '.' . $this->databaseBackupExtensionFromEntry($entry, $prefix);
    }

    /**
     * @param array<string,mixed> $entry
     */
    private function databaseBackupExtensionFromEntry(array $entry, string $prefix = ''): string
    {
        $fileName = (string) ($entry[$prefix . 'file_name'] ?? '');
        if ($fileName !== '') {
            $extension = strtolower(pathinfo($fileName, PATHINFO_EXTENSION));
            if ($extension === 'zip' || $extension === 'json') {
                return $extension;
            }
        }

        $format = strtolower((string) ($entry[$prefix . 'archive_format'] ?? ''));

        return $format === 'zip' ? 'zip' : 'json';
    }

    private function alternateDatabaseBackupPath(string $backupId, string $currentPath): string
    {
        $backupId = strtolower(trim($backupId));
        if (!preg_match('/^db-[0-9]{8}-[0-9]{6}-[a-f0-9]{32}$/', $backupId)) {
            return '';
        }

        $extension = strtolower(pathinfo($currentPath, PATHINFO_EXTENSION));
        $alternate = $extension === 'zip' ? 'json' : 'zip';

        return $this->databaseBackupDirectory() . '/' . $backupId . '.' . $alternate;
    }

    private function databaseBackupArchiveFormat(string $path): string
    {
        return strtolower(pathinfo($path, PATHINFO_EXTENSION)) === 'zip' ? 'zip' : 'json';
    }

    private function databaseBackupMimeType(string $path): string
    {
        return strtolower(pathinfo($path, PATHINFO_EXTENSION)) === 'zip' ? 'application/zip' : 'application/json';
    }

    private function restorePointDirectory(): string
    {
        return $this->storage->baseDir() . '/restore-points';
    }

    private function siteFolderName(): string
    {
        $url = function_exists('home_url') ? (string) home_url('/') : (function_exists('site_url') ? (string) site_url('/') : '');
        $host = '';
        $path = '';
        if ($url !== '') {
            $parsed = function_exists('wp_parse_url') ? wp_parse_url($url) : [];
            if (is_array($parsed)) {
                $host = isset($parsed['host']) ? (string) $parsed['host'] : '';
                $path = isset($parsed['path']) ? trim((string) $parsed['path'], '/') : '';
            }
        }

        $name = trim($host . ($path !== '' ? '-' . $path : ''));
        if ($name === '') {
            $name = 'wordpress-site';
        }

        $name = preg_replace('/[^A-Za-z0-9._-]+/', '-', strtolower($name)) ?: 'wordpress-site';

        return substr(trim($name, '-'), 0, 120) ?: 'wordpress-site';
    }

    private function createdAtFromBackupFile(string $path): string
    {
        $decoded = $this->readJson($path);

        return (string) ($decoded['created_at'] ?? '');
    }

    /**
     * @return array<string,mixed>
     */
    private function readJson(string $path): array
    {
        if (!is_file($path) || !is_readable($path)) {
            return [];
        }

        $raw = file_get_contents($path);
        $decoded = is_string($raw) ? \ShieldX\Support\Json::decodeArray($raw) : null;

        return is_array($decoded) ? $decoded : [];
    }

    private function cleanRelativePath(string $path): string
    {
        $path = trim(str_replace('\\', '/', $path), '/');
        if ($path === '' || strpos($path, '..') !== false || strpos($path, "\0") !== false) {
            return '';
        }

        return $path;
    }

    /**
     * @param array<int,mixed> $warnings
     * @return array<int,string>
     */
    private function sanitizeWarningDetails(array $warnings): array
    {
        $clean = [];
        foreach ($warnings as $warning) {
            $warning = trim(preg_replace('/[\r\n\t]+/', ' ', (string) $warning) ?: '');
            if ($warning === '') {
                continue;
            }

            $clean[] = substr($warning, 0, 240);
            if (count($clean) >= 20) {
                break;
            }
        }

        return array_values(array_unique($clean));
    }

    private function temporaryToken(): string
    {
        try {
            return bin2hex(random_bytes(16));
        } catch (\Throwable $error) {
            return '';
        }
    }

    private function mimeTypeForPath(string $path): string
    {
        $extension = strtolower(pathinfo($path, PATHINFO_EXTENSION));
        if ($extension === 'zip') {
            return 'application/zip';
        }

        if ($extension === 'json') {
            return 'application/json';
        }

        if ($extension === 'txt' || $extension === 'log') {
            return 'text/plain';
        }

        return 'application/octet-stream';
    }

    private function deleteLocalFileIfSafe(string $path, string $allowedRoot): void
    {
        $real = realpath($path);
        $root = realpath($allowedRoot);
        if (!is_string($real) || !is_string($root) || !$this->pathInside($real, $root) || !is_file($real)) {
            return;
        }

        $this->deleteFile($real);
    }

    private function deleteLocalDirectoryIfSafe(string $path, string $allowedRoot): void
    {
        $real = realpath($path);
        $root = realpath($allowedRoot);
        if (!is_string($real) || !is_string($root) || !$this->pathInside($real, $root) || !is_dir($real)) {
            return;
        }

        $this->deleteDirectory($real);
    }

    private function ensureDirectory(string $directory): void
    {
        if (is_dir($directory)) {
            return;
        }

        if (function_exists('wp_mkdir_p')) {
            wp_mkdir_p($directory);
        } else {
            mkdir($directory, 0755, true);
        }
    }

    private function pathInside(string $path, string $root): bool
    {
        $path = rtrim(str_replace('\\', '/', $path), '/');
        $root = rtrim(str_replace('\\', '/', $root), '/');

        return $path === $root || strpos($path, $root . '/') === 0;
    }

    private function deleteDirectory(string $path): void
    {
        $items = scandir($path);
        if (!is_array($items)) {
            return;
        }

        foreach ($items as $item) {
            if ($item === '.' || $item === '..') {
                continue;
            }

            $child = $path . DIRECTORY_SEPARATOR . $item;
            if (is_dir($child) && !is_link($child)) {
                $this->deleteDirectory($child);
            } else {
                $this->deleteFile($child);
            }
        }

        rmdir($path);
    }

    private function deleteFile(string $path): void
    {
        if (is_file($path) || is_link($path)) {
            unlink($path);
        }
    }

    private static function now(): string
    {
        return function_exists('current_time') ? current_time('mysql', true) : gmdate('Y-m-d H:i:s');
    }
}
