<?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_chmod,WordPress.WP.AlternativeFunctions.file_system_operations_tempnam,WordPress.WP.AlternativeFunctions.unlink_unlink,WordPress.WP.AlternativeFunctions.file_system_operations_fopen,WordPress.WP.AlternativeFunctions.file_system_operations_fread,WordPress.WP.AlternativeFunctions.file_system_operations_fclose,WordPress.WP.AlternativeFunctions.file_system_operations_mkdir,WordPress.WP.AlternativeFunctions.rename_rename -- Google Drive backup transfer streams private SHIELD-X backup files and writes same-directory temp files atomically.

final class GoogleDriveClient
{
    private const DEVICE_CODE_ENDPOINT = 'https://oauth2.googleapis.com/device/code';
    private const TOKEN_ENDPOINT = 'https://oauth2.googleapis.com/token';
    private const REVOKE_ENDPOINT = 'https://oauth2.googleapis.com/revoke';
    private const DRIVE_ENDPOINT = 'https://www.googleapis.com/drive/v3';
    private const DRIVE_UPLOAD_ENDPOINT = 'https://www.googleapis.com/upload/drive/v3';
    private const FOLDER_MIME = 'application/vnd.google-apps.folder';
    private const CHUNK_BYTES = 5242880;

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

    /** @var callable|null */
    private $http;

    public function __construct(GoogleDriveSettings $settings, ?callable $http = null)
    {
        $this->settings = $settings;
        $this->http = $http;
    }

    /**
     * @return array<string,mixed>
     */
    public function startDeviceAuthorization(): array
    {
        if (!$this->settings->credentialsAvailable()) {
            return $this->error('Google Drive sign-in is missing the internal SHIELD-X OAuth app configuration.');
        }

        $response = $this->request('POST', self::DEVICE_CODE_ENDPOINT, [
            'headers' => ['Content-Type' => 'application/x-www-form-urlencoded'],
            'body' => http_build_query([
                'client_id' => $this->settings->clientId(),
                'scope' => GoogleDriveSettings::DRIVE_SCOPE,
            ], '', '&'),
        ]);

        $decoded = $this->decodeResponse($response);
        if (!$this->isHttpOk($response['code'] ?? 0) || empty($decoded['device_code'])) {
            return $this->googleError($decoded, 'Google did not start device authorization.');
        }

        return [
            'ok' => true,
            'device_code' => (string) ($decoded['device_code'] ?? ''),
            'user_code' => (string) ($decoded['user_code'] ?? ''),
            'verification_url' => (string) ($decoded['verification_url'] ?? $decoded['verification_uri'] ?? ''),
            'expires_in' => (int) ($decoded['expires_in'] ?? 1800),
            'interval' => (int) ($decoded['interval'] ?? 5),
        ];
    }

    /**
     * @return array<string,mixed>
     */
    public function completeDeviceAuthorization(string $deviceCode): array
    {
        if (!$this->settings->credentialsAvailable()) {
            return $this->error('Google Drive sign-in is missing the internal SHIELD-X OAuth app configuration.');
        }

        $response = $this->request('POST', self::TOKEN_ENDPOINT, [
            'headers' => ['Content-Type' => 'application/x-www-form-urlencoded'],
            'body' => http_build_query([
                'client_id' => $this->settings->clientId(),
                'client_secret' => $this->settings->clientSecret(),
                'device_code' => $deviceCode,
                'grant_type' => 'urn:ietf:params:oauth:grant-type:device_code',
            ], '', '&'),
        ]);

        $decoded = $this->decodeResponse($response);
        if (!$this->isHttpOk($response['code'] ?? 0)) {
            $error = (string) ($decoded['error'] ?? '');
            if (in_array($error, ['authorization_pending', 'slow_down'], true)) {
                return [
                    'ok' => false,
                    'status' => $error,
                    'message' => $error === 'slow_down'
                        ? 'Google asked SHIELD-X to wait before checking again.'
                        : 'Google authorization is still pending.',
                ];
            }

            return $this->googleError($decoded, 'Google authorization failed.');
        }

        return [
            'ok' => true,
            'access_token' => (string) ($decoded['access_token'] ?? ''),
            'refresh_token' => (string) ($decoded['refresh_token'] ?? ''),
            'expires_in' => (int) ($decoded['expires_in'] ?? 3600),
        ];
    }

    /**
     * @return array<string,mixed>
     */
    public function revokeRefreshToken(): array
    {
        $refreshToken = $this->settings->refreshToken();
        if ($refreshToken === '') {
            return [
                'ok' => true,
                'skipped' => true,
                'message' => 'No Google Drive refresh token was stored.',
            ];
        }

        $response = $this->request('POST', self::REVOKE_ENDPOINT, [
            'headers' => ['Content-Type' => 'application/x-www-form-urlencoded'],
            'body' => http_build_query(['token' => $refreshToken], '', '&'),
        ]);

        $decoded = $this->decodeResponse($response);
        if (!$this->isHttpOk($response['code'] ?? 0)) {
            return $this->googleError($decoded, 'Google Drive token revocation failed.');
        }

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

    /**
     * @return array<string,mixed>
     */
    public function about(): array
    {
        $token = $this->accessToken();
        if (empty($token['ok'])) {
            return $token;
        }

        $response = $this->authorizedRequest('GET', self::DRIVE_ENDPOINT . '/about?fields=user', (string) $token['access_token']);
        $decoded = $this->decodeResponse($response);
        if (!$this->isHttpOk($response['code'] ?? 0)) {
            return $this->googleError($decoded, 'Google Drive connection test failed.');
        }

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

        return [
            'ok' => true,
            'email' => (string) ($user['emailAddress'] ?? ''),
        ];
    }

    /**
     * @return array<string,mixed>
     */
    public function ensureRootFolder(): array
    {
        $token = $this->accessToken();
        if (empty($token['ok'])) {
            return $token;
        }

        return $this->ensureFolderWithToken('SHIELD-X Backups', 'root', (string) $token['access_token']);
    }

    /**
     * @param array<int,string> $segments
     * @return array<string,mixed>
     */
    public function ensureBackupFolderPath(array $segments): array
    {
        $token = $this->accessToken();
        if (empty($token['ok'])) {
            return $token;
        }

        $rootId = $this->settings->rootFolderId();
        if ($rootId === '') {
            $root = $this->ensureFolderWithToken('SHIELD-X Backups', 'root', (string) $token['access_token']);
            if (empty($root['ok'])) {
                return $root;
            }
            $rootId = (string) ($root['id'] ?? '');
        }

        $parentId = $rootId;
        foreach ($segments as $segment) {
            $folder = $this->ensureFolderWithToken($this->cleanDriveName($segment), $parentId, (string) $token['access_token']);
            if (empty($folder['ok'])) {
                return $folder;
            }
            $parentId = (string) ($folder['id'] ?? '');
        }

        return [
            'ok' => true,
            'id' => $parentId,
        ];
    }

    /**
     * @return array<string,mixed>
     */
    public function ensureFolder(string $name, string $parentId): array
    {
        $token = $this->accessToken();
        if (empty($token['ok'])) {
            return $token;
        }

        return $this->ensureFolderWithToken($this->cleanDriveName($name), $parentId, (string) $token['access_token']);
    }

    /**
     * @return array<string,mixed>
     */
    public function uploadFileResumable(string $path, string $name, string $parentId, string $mimeType = 'application/octet-stream'): array
    {
        if (!is_file($path) || !is_readable($path)) {
            return $this->error('Local backup file is not readable.');
        }

        $token = $this->accessToken();
        if (empty($token['ok'])) {
            return $token;
        }

        $size = (int) filesize($path);
        $metadata = [
            'name' => $this->cleanDriveName($name),
            'parents' => [$parentId],
        ];
        $metadataJson = wp_json_encode($metadata, JSON_UNESCAPED_SLASHES);
        if (!is_string($metadataJson)) {
            return $this->error('Could not prepare Google Drive metadata.');
        }

        $session = $this->authorizedRequest(
            'POST',
            self::DRIVE_UPLOAD_ENDPOINT . '/files?uploadType=resumable&fields=id,name,size,md5Checksum',
            (string) $token['access_token'],
            [
                'headers' => [
                    'Content-Type' => 'application/json; charset=UTF-8',
                    'X-Upload-Content-Type' => $mimeType,
                    'X-Upload-Content-Length' => (string) $size,
                ],
                'body' => $metadataJson,
            ]
        );

        if (!in_array((int) ($session['code'] ?? 0), [200, 201], true)) {
            return $this->googleError($this->decodeResponse($session), 'Google Drive upload session failed.');
        }

        $uploadUrl = $this->headerValue($session['headers'] ?? [], 'location');
        if ($uploadUrl === '') {
            return $this->error('Google Drive did not return an upload URL.');
        }

        $handle = fopen($path, 'rb');
        if (!is_resource($handle)) {
            return $this->error('Could not open local backup file for upload.');
        }

        $offset = 0;
        $final = [];
        try {
            if ($size === 0) {
                $final = $this->requestWithRetries('PUT', $uploadUrl, [
                    'headers' => [
                        'Content-Length' => '0',
                        'Content-Range' => 'bytes */0',
                    ],
                    'body' => '',
                    'timeout' => 120,
                ]);
            } else {
                while ($offset < $size) {
                    $length = min(self::CHUNK_BYTES, $size - $offset);
                    if (fseek($handle, $offset) !== 0) {
                        return $this->error('Could not seek local backup file during upload.');
                    }

                    $chunk = fread($handle, $length);
                    if (!is_string($chunk)) {
                        return $this->error('Could not read local backup file during upload.');
                    }

                    $end = $offset + strlen($chunk) - 1;
                    $response = $this->requestWithRetries('PUT', $uploadUrl, [
                        'headers' => [
                            'Content-Length' => (string) strlen($chunk),
                            'Content-Range' => 'bytes ' . $offset . '-' . $end . '/' . $size,
                        ],
                        'body' => $chunk,
                        'timeout' => 120,
                    ]);

                    $code = (int) ($response['code'] ?? 0);
                    if ($code === 308) {
                        $confirmedOffset = $this->uploadRangeOffset($response['headers'] ?? [], $offset);
                        if ($confirmedOffset <= $offset && strlen($chunk) > 0) {
                            return $this->error('Google Drive upload did not confirm chunk progress.');
                        }

                        $offset = min($size, $confirmedOffset);
                        continue;
                    }

                    if (in_array($code, [200, 201], true)) {
                        $final = $response;
                        break;
                    }

                    return $this->googleError($this->decodeResponse($response), 'Google Drive upload failed.');
                }
            }
        } finally {
            fclose($handle);
        }

        $decoded = $this->decodeResponse($final);
        if (empty($decoded['id'])) {
            return $this->error('Google Drive upload could not be verified.');
        }

        return [
            'ok' => true,
            'id' => (string) ($decoded['id'] ?? ''),
            'name' => (string) ($decoded['name'] ?? $name),
            'size' => (int) ($decoded['size'] ?? $size),
            'md5Checksum' => (string) ($decoded['md5Checksum'] ?? ''),
        ];
    }

    /**
     * @param mixed $headers
     */
    private function uploadRangeOffset($headers, int $fallbackOffset): int
    {
        $range = $this->headerValue($headers, 'range');
        if (preg_match('/bytes=0-([0-9]+)/i', $range, $matches) === 1) {
            return ((int) $matches[1]) + 1;
        }

        return $fallbackOffset;
    }

    /**
     * @return array<string,mixed>
     */
    public function downloadFile(string $fileId, string $targetPath): array
    {
        $fileId = $this->cleanId($fileId);
        if ($fileId === '') {
            return $this->error('Google Drive file identifier is missing.');
        }

        $token = $this->accessToken();
        if (empty($token['ok'])) {
            return $token;
        }

        $directory = dirname($targetPath);
        if (!is_dir($directory)) {
            if (function_exists('wp_mkdir_p')) {
                wp_mkdir_p($directory);
            } else {
                mkdir($directory, 0755, true);
            }
        }

        $temporary = tempnam($directory, 'shieldx-drive-');
        if (!is_string($temporary) || $temporary === '') {
            return $this->error('Could not create a local restore temp file.');
        }

        if ($this->http === null && function_exists('wp_remote_get')) {
            $response = $this->authorizedRequest(
                'GET',
                self::DRIVE_ENDPOINT . '/files/' . rawurlencode($fileId) . '?alt=media',
                (string) $token['access_token'],
                [
                    'stream' => true,
                    'filename' => $temporary,
                    'timeout' => 120,
                ]
            );
            if (!$this->isHttpOk($response['code'] ?? 0) || !is_file($temporary)) {
                $this->removeTemporary($temporary);
                return $this->googleError($this->decodeResponse($response), 'Google Drive download failed.');
            }
        } else {
            $response = $this->authorizedRequest(
                'GET',
                self::DRIVE_ENDPOINT . '/files/' . rawurlencode($fileId) . '?alt=media',
                (string) $token['access_token'],
                ['timeout' => 120]
            );
            if (!$this->isHttpOk($response['code'] ?? 0)) {
                $this->removeTemporary($temporary);
                return $this->googleError($this->decodeResponse($response), 'Google Drive download failed.');
            }
            if (file_put_contents($temporary, (string) ($response['body'] ?? ''), LOCK_EX) === false) {
                $this->removeTemporary($temporary);
                return $this->error('Could not write the restored Google Drive file locally.');
            }
        }

        chmod($temporary, 0600);
        if (!rename($temporary, $targetPath)) {
            $this->removeTemporary($temporary);
            return $this->error('Could not move the restored Google Drive file into place.');
        }
        chmod($targetPath, 0600);

        return [
            'ok' => true,
            'path' => $targetPath,
        ];
    }

    /**
     * @return array<string,mixed>
     */
    public function fileAvailable(string $fileId): array
    {
        $fileId = $this->cleanId($fileId);
        if ($fileId === '') {
            return $this->error('Google Drive file identifier is missing.');
        }

        $token = $this->accessToken();
        if (empty($token['ok'])) {
            return $token;
        }

        $response = $this->authorizedRequest(
            'GET',
            self::DRIVE_ENDPOINT . '/files/' . rawurlencode($fileId) . '?fields=id,name,size,md5Checksum,trashed',
            (string) $token['access_token']
        );
        $decoded = $this->decodeResponse($response);
        if (!$this->isHttpOk($response['code'] ?? 0)) {
            return $this->googleError($decoded, 'Google Drive file is not available.');
        }

        if (!empty($decoded['trashed'])) {
            return $this->error('Google Drive file is in trash.');
        }

        return [
            'ok' => true,
            'id' => (string) ($decoded['id'] ?? $fileId),
            'name' => (string) ($decoded['name'] ?? ''),
            'size' => (int) ($decoded['size'] ?? 0),
            'md5Checksum' => (string) ($decoded['md5Checksum'] ?? ''),
        ];
    }

    /**
     * @return array<string,mixed>
     */
    public function deleteFile(string $fileId): array
    {
        $fileId = $this->cleanId($fileId);
        if ($fileId === '') {
            return $this->error('Google Drive file identifier is missing.');
        }

        $token = $this->accessToken();
        if (empty($token['ok'])) {
            return $token;
        }

        $response = $this->authorizedRequest('DELETE', self::DRIVE_ENDPOINT . '/files/' . rawurlencode($fileId), (string) $token['access_token']);
        $code = (int) ($response['code'] ?? 0);
        if ($code === 204 || $code === 404) {
            return [
                'ok' => true,
                'deleted' => $code === 204,
            ];
        }

        return $this->googleError($this->decodeResponse($response), 'Google Drive file cleanup failed.');
    }

    /**
     * @return array<string,mixed>
     */
    private function accessToken(): array
    {
        $refreshToken = $this->settings->refreshToken();
        if ($refreshToken === '') {
            return $this->error('Google Drive is not connected.');
        }

        $cachedToken = $this->settings->cachedAccessToken();
        if ($cachedToken !== '') {
            return [
                'ok' => true,
                'access_token' => $cachedToken,
            ];
        }

        $response = $this->request('POST', self::TOKEN_ENDPOINT, [
            'headers' => ['Content-Type' => 'application/x-www-form-urlencoded'],
            'body' => http_build_query([
                'client_id' => $this->settings->clientId(),
                'client_secret' => $this->settings->clientSecret(),
                'refresh_token' => $refreshToken,
                'grant_type' => 'refresh_token',
            ], '', '&'),
        ]);

        $decoded = $this->decodeResponse($response);
        if (!$this->isHttpOk($response['code'] ?? 0) || empty($decoded['access_token'])) {
            return $this->googleError($decoded, 'Google Drive token refresh failed.');
        }

        $accessToken = (string) ($decoded['access_token'] ?? '');
        $this->settings->cacheAccessToken($accessToken, (int) ($decoded['expires_in'] ?? 3600));

        return [
            'ok' => true,
            'access_token' => $accessToken,
        ];
    }

    /**
     * @param array<string,mixed> $extra
     * @return array<string,mixed>
     */
    private function authorizedRequest(string $method, string $url, string $accessToken, array $extra = []): array
    {
        $headers = isset($extra['headers']) && is_array($extra['headers']) ? $extra['headers'] : [];
        $headers['Authorization'] = 'Bearer ' . $accessToken;
        $extra['headers'] = $headers;

        return $this->requestWithRetries($method, $url, $extra);
    }

    /**
     * @return array<string,mixed>
     */
    private function ensureFolderWithToken(string $name, string $parentId, string $accessToken): array
    {
        $parentId = $this->cleanId($parentId);
        if ($parentId === '') {
            return $this->error('Google Drive parent folder is missing.');
        }

        $query = sprintf(
            'name = %s and mimeType = %s and trashed = false and %s in parents',
            $this->driveQueryLiteral($name),
            $this->driveQueryLiteral(self::FOLDER_MIME),
            $this->driveQueryLiteral($parentId)
        );
        $url = self::DRIVE_ENDPOINT . '/files?fields=files(id,name)&q=' . rawurlencode($query);
        $response = $this->authorizedRequest('GET', $url, $accessToken);
        $decoded = $this->decodeResponse($response);
        if (!$this->isHttpOk($response['code'] ?? 0)) {
            return $this->googleError($decoded, 'Google Drive folder lookup failed.');
        }

        $files = isset($decoded['files']) && is_array($decoded['files']) ? $decoded['files'] : [];
        foreach ($files as $file) {
            if (is_array($file) && !empty($file['id'])) {
                return [
                    'ok' => true,
                    'id' => (string) $file['id'],
                    'created' => false,
                ];
            }
        }

        $metadata = wp_json_encode([
            'name' => $name,
            'mimeType' => self::FOLDER_MIME,
            'parents' => [$parentId],
        ], JSON_UNESCAPED_SLASHES);
        if (!is_string($metadata)) {
            return $this->error('Could not prepare Google Drive folder metadata.');
        }

        $create = $this->authorizedRequest('POST', self::DRIVE_ENDPOINT . '/files?fields=id,name', $accessToken, [
            'headers' => ['Content-Type' => 'application/json; charset=UTF-8'],
            'body' => $metadata,
        ]);
        $created = $this->decodeResponse($create);
        if (!$this->isHttpOk($create['code'] ?? 0) || empty($created['id'])) {
            return $this->googleError($created, 'Google Drive folder creation failed.');
        }

        return [
            'ok' => true,
            'id' => (string) ($created['id'] ?? ''),
            'created' => true,
        ];
    }

    /**
     * @param array<string,mixed> $args
     * @return array{code:int,body:string,headers:array<string,mixed>,error:string}
     */
    private function request(string $method, string $url, array $args = []): array
    {
        $method = strtoupper($method);
        $args = array_merge([
            'timeout' => 45,
            'redirection' => 0,
            'reject_unsafe_urls' => true,
            'headers' => [],
            'body' => null,
        ], $args);

        if ($this->http !== null) {
            try {
                $response = call_user_func($this->http, $method, $url, $args);
            } catch (\Throwable $error) {
                return [
                    'code' => 0,
                    'body' => '',
                    'headers' => [],
                    'error' => $error->getMessage(),
                ];
            }

            return $this->normalizeHttpResponse(is_array($response) ? $response : []);
        }

        if (!function_exists('wp_remote_request')) {
            return [
                'code' => 0,
                'body' => '',
                'headers' => [],
                'error' => 'WordPress HTTP API is not available.',
            ];
        }

        $args['method'] = $method;
        try {
            $response = wp_remote_request($url, $args);
        } catch (\Throwable $error) {
            return [
                'code' => 0,
                'body' => '',
                'headers' => [],
                'error' => $error->getMessage(),
            ];
        }

        if (function_exists('is_wp_error') && is_wp_error($response)) {
            return [
                'code' => 0,
                'body' => '',
                'headers' => [],
                'error' => method_exists($response, 'get_error_message') ? (string) $response->get_error_message() : 'HTTP request failed.',
            ];
        }

        return [
            'code' => function_exists('wp_remote_retrieve_response_code') ? (int) wp_remote_retrieve_response_code($response) : 0,
            'body' => function_exists('wp_remote_retrieve_body') ? (string) wp_remote_retrieve_body($response) : '',
            'headers' => function_exists('wp_remote_retrieve_headers') ? $this->normalizeHeaders(wp_remote_retrieve_headers($response)) : [],
            'error' => '',
        ];
    }

    /**
     * @param array<string,mixed> $args
     * @return array{code:int,body:string,headers:array<string,mixed>,error:string}
     */
    private function requestWithRetries(string $method, string $url, array $args = [], int $attempts = 3): array
    {
        $attempts = max(1, min(5, $attempts));
        $response = [
            'code' => 0,
            'body' => '',
            'headers' => [],
            'error' => '',
        ];

        for ($attempt = 1; $attempt <= $attempts; $attempt++) {
            $response = $this->request($method, $url, $args);
            $code = (int) ($response['code'] ?? 0);
            if ($code !== 0 && $code !== 429 && $code < 500) {
                return $response;
            }

            if ($attempt < $attempts) {
                usleep(100000 * $attempt);
            }
        }

        return $response;
    }

    /**
     * @param array<string,mixed> $response
     * @return array{code:int,body:string,headers:array<string,mixed>,error:string}
     */
    private function normalizeHttpResponse(array $response): array
    {
        return [
            'code' => (int) ($response['code'] ?? $response['status'] ?? 0),
            'body' => is_scalar($response['body'] ?? '') ? (string) ($response['body'] ?? '') : '',
            'headers' => $this->normalizeHeaders($response['headers'] ?? []),
            'error' => is_scalar($response['error'] ?? '') ? (string) ($response['error'] ?? '') : '',
        ];
    }

    /**
     * WordPress may return headers as a Requests CaseInsensitiveDictionary object.
     *
     * @param mixed $headers
     * @return array<string,string>
     */
    private function normalizeHeaders($headers): array
    {
        if (is_object($headers)) {
            if (method_exists($headers, 'getAll')) {
                $headers = $headers->getAll();
            } elseif (method_exists($headers, 'get_all')) {
                $headers = $headers->get_all();
            } elseif (method_exists($headers, 'toArray')) {
                $headers = $headers->toArray();
            } elseif ($headers instanceof \Traversable) {
                $headers = iterator_to_array($headers);
            } else {
                $headers = (array) $headers;
            }
        }

        if (!is_array($headers)) {
            return [];
        }

        $normalized = [];
        foreach ($headers as $key => $value) {
            $name = strtolower(trim((string) $key));
            if ($name === '') {
                continue;
            }

            if (is_array($value)) {
                $parts = [];
                foreach ($value as $part) {
                    if (is_scalar($part)) {
                        $parts[] = trim((string) $part);
                    }
                }
                $value = implode(', ', array_filter($parts, static function (string $part): bool {
                    return $part !== '';
                }));
            }

            if (!is_scalar($value)) {
                continue;
            }

            $normalized[$name] = trim((string) $value);
        }

        return $normalized;
    }

    /**
     * @param array<string,mixed> $response
     * @return array<string,mixed>
     */
    private function decodeResponse(array $response): array
    {
        $body = (string) ($response['body'] ?? '');
        if ($body === '') {
            return [];
        }

        $decoded = \ShieldX\Support\Json::decodeArray($body);

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

    /**
     * @param array<string,mixed> $headers
     */
    private function headerValue(array $headers, string $name): string
    {
        foreach ($headers as $key => $value) {
            if (strtolower((string) $key) !== strtolower($name)) {
                continue;
            }

            if (is_array($value)) {
                $value = reset($value);
            }

            return is_scalar($value) ? trim((string) $value) : '';
        }

        return '';
    }

    /**
     * @param mixed $code
     */
    private function isHttpOk($code): bool
    {
        $code = (int) $code;

        return $code >= 200 && $code < 300;
    }

    /**
     * @return array{ok:false,message:string,status:string}
     */
    private function error(string $message, string $status = 'error'): array
    {
        return [
            'ok' => false,
            'status' => $status,
            'message' => $message,
        ];
    }

    /**
     * @param array<string,mixed> $decoded
     * @return array{ok:false,message:string,status:string}
     */
    private function googleError(array $decoded, string $fallback): array
    {
        $message = '';
        if (isset($decoded['error_description']) && is_scalar($decoded['error_description'])) {
            $message = (string) $decoded['error_description'];
        } elseif (isset($decoded['error']['message']) && is_scalar($decoded['error']['message'])) {
            $message = (string) $decoded['error']['message'];
        } elseif (isset($decoded['error']) && is_scalar($decoded['error'])) {
            $message = (string) $decoded['error'];
        }

        if ($this->isWrongDeviceClientError($message)) {
            return $this->error('This Google OAuth client cannot use device authorization. In Google Cloud, create an OAuth client with application type "TVs and Limited Input devices", then save that client ID and client secret in SHIELD-X. Do not use Web application or Desktop app credentials.');
        }

        if ($this->isDeviceCodeAlreadyExchangedError($message)) {
            return $this->error('This Google authorization code was already used. Start Google authentication again to get a fresh code.');
        }

        return $this->error($message !== '' ? $message : $fallback);
    }

    private function isWrongDeviceClientError(string $message): bool
    {
        $message = strtolower($message);

        return strpos($message, 'only clients of type') !== false
            && (strpos($message, 'limited input') !== false || strpos($message, 'limited-input') !== false);
    }

    private function isDeviceCodeAlreadyExchangedError(string $message): bool
    {
        $message = strtolower($message);

        return strpos($message, 'device code') !== false
            && strpos($message, 'already exchanged') !== false;
    }

    private function cleanDriveName(string $name): string
    {
        $name = trim(preg_replace('/[\x00-\x1F\x7F\/\\\\]+/', '-', $name) ?: '');
        $name = trim($name, " .\t\n\r\0\x0B");

        return substr($name !== '' ? $name : 'backup', 0, 160);
    }

    private function cleanId(string $id): string
    {
        $id = trim($id);

        return preg_match('/^[A-Za-z0-9_\-]+$/', $id) === 1 ? $id : '';
    }

    private function driveQueryLiteral(string $value): string
    {
        return "'" . str_replace(["\\", "'"], ["\\\\", "\\'"], $value) . "'";
    }

    private function removeTemporary(string $path): void
    {
        if (is_file($path)) {
            unlink($path);
        }
    }
}
