<?php

namespace WPStaging\Pro\Backup\Storage\Storages\GoogleDrive;

use Exception;
use WPStaging\Framework\Filesystem\FileObject;
use WPStaging\Framework\Queue\FinishedQueueException;
use WPStaging\Framework\Utils\Strings;
use WPStaging\Pro\Backup\Dto\Job\JobExportDataDto;
use WPStaging\Pro\Backup\Dto\StepsDto;
use WPStaging\Pro\Backup\Storage\RemoteUploaderInterface;
use WPStaging\Vendor\Google\Client as GoogleClient;
use WPStaging\Vendor\Google\Service\Drive as GoogleDriveService;
use WPStaging\Vendor\Google\Service\Drive\DriveFile as GoogleDriveFile;
use WPStaging\Vendor\Google\Http\MediaFileUpload as GoogleMediaFileUpload;
use WPStaging\Vendor\Psr\Log\LoggerInterface;

use function WPStaging\functions\debug_log;

class Uploader implements RemoteUploaderInterface
{
    /** @var GoogleClient */
    private $client;

    /** @var StepsDto */
    private $stepsDto;

    /** @var JobExportDataDto */
    private $jobDataDto;

    /** @var LoggerInterface */
    private $logger;

    /** @var string */
    private $filePath;

    /** @var string|null */
    private $folderId;

    /** @var string */
    private $folderName;

    /** @var int */
    private $maxBackupsToKeep;

    /** @var GoogleDriveService */
    private $service;

    /** @var FileObject */
    private $fileObject;

    /** @var GoogleMediaFileUpload */
    private $media;

    /** @var int */
    private $chunkSize;

    /** @var Auth */
    private $auth;

    /** @var bool|string */
    private $error;

    /** @var Strings */
    private $strings;

    public function __construct(Auth $auth, Strings $strings)
    {
        $this->error = false;
        $this->auth = $auth;
        $this->strings = $strings;

        if (!$this->auth->isGuzzleAvailable()) {
            $this->error = __('cURL extension is missing. Backup is still available locally.', 'wp-staging');
            return;
        }

        if (!$this->auth->isAuthenticated()) {
            $this->error = __('Google Drive is not authenticated. Backup is still available locally.', 'wp-staging');
            return;
        }

        $this->client = $auth->setClientWithAuthToken();
        $options = $this->auth->getOptions();
        $this->folderName = isset($options['folderName']) ? $options['folderName'] : Auth::FOLDER_NAME;
        $this->maxBackupsToKeep = isset($options['maxBackupsToKeep']) ? $options['maxBackupsToKeep'] : 15;
        $this->maxBackupsToKeep = intval($this->maxBackupsToKeep);
        $this->maxBackupsToKeep = $this->maxBackupsToKeep > 0 ? $this->maxBackupsToKeep : 15;
        $this->folderId = null;
    }

    public function getProviderName()
    {
        return 'Google Drive';
    }

    public function setupUpload(LoggerInterface $logger, StepsDto $stepsDto, JobExportDataDto $jobDataDto, $chunkSize = 1 * 1024 * 1024)
    {
        $this->logger = $logger;
        $this->stepsDto = $stepsDto;
        $this->jobDataDto = $jobDataDto;
        $this->chunkSize = $chunkSize;
    }

    public function setBackupFilePath($backupFilePath)
    {
        $this->filePath = $backupFilePath;
        $this->fileObject = new FileObject($this->filePath, FileObject::MODE_READ);
        $this->service = new GoogleDriveService($this->client);

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

        $this->folderId = $this->auth->getFolderIdByName($this->folderName, $this->service);

        if (!$this->stepsDto->getTotal()) {
            if ($this->deleteOldestBackups()) {
                $this->logger->info('Google Drive - Deleted oldest backup file');
            }
        }

        $fileMetadata = new GoogleDriveFile([
            'name' => $this->fileObject->getBasename(),
            'parents' => [$this->folderId],
        ]);

        $this->client->setDefer(true);

        $request = $this->service->files->create($fileMetadata);
        $this->media = new GoogleMediaFileUpload(
            $this->client,
            $request,
            'application/octet-stream',
            null,
            true,
            $this->chunkSize
        );

        $this->media->setFileSize($this->fileObject->getSize());

        if (!$this->stepsDto->getTotal()) {
            $this->stepsDto->setTotal($this->fileObject->getSize());
            $this->stepsDto->setCurrent(0);
            $this->jobDataDto->setRemoteStorageMeta([
                'ResumeURI' => $this->media->getResumeUri()
            ]);
            $this->logger->info('Google Drive Initiate backup upload to ' . $this->getProviderName());
            return true;
        }

        $resumeURI = $this->jobDataDto->getRemoteStorageMeta()['ResumeURI'];
        $this->media->resume($resumeURI);
        $newResumeURI = $this->media->getResumeUri();
        if ($newResumeURI !== $resumeURI) {
            $this->jobDataDto->setRemoteStorageMeta([
                'ResumeURI' => $newResumeURI
            ]);
        }

        return true;
    }

    /**
     * @param string $filePath
     * @param StepsDto $stepsDto
     * @param int $chunkSize
     *
     * @return int
     */
    public function chunkUpload()
    {
        $status = false;
        $offset = $this->stepsDto->getCurrent();

        $this->fileObject->fseek($offset);
        $chunk = $this->fileObject->fread($this->chunkSize);
        $status = $this->media->nextChunk($chunk);

        $offset += strlen($chunk);

        if ($status !== false) {
            throw new FinishedQueueException();
        }

        $this->stepsDto->setCurrent($offset);

        return $offset;
    }

        /**
     * @param string $filePath
     * @return bool
     */
    public function uploadFile($filePath)
    {
        $fileObject = new FileObject($filePath, FileObject::MODE_READ);

        $this->service = new GoogleDriveService($this->client);

        if ($this->folderId === null) {
            $this->folderId = $this->auth->getFolderIdByName($this->folderName);
        }

        $fileMetadata = new GoogleDriveFile([
            'name' => $fileObject->getBasename(),
            'parents' => [$this->folderId],
        ]);

        $this->client->setDefer(true);

        $request = $this->service->files->create($fileMetadata);
        $this->media = new GoogleMediaFileUpload(
            $this->client,
            $request,
            'application/octet-stream',
            null,
            true,
            $fileObject->getSize()
        );

        $this->media->setFileSize($fileObject->getSize());

        $fileObject->fseek(0);
        $chunk = $fileObject->fread($fileObject->getSize());

        try {
            $status = $this->media->nextChunk($chunk);
        } catch (Exception $ex) {
            debug_log("Error: " . $ex->getMessage());
            return false;
        }

        return true;
    }

    public function stopUpload()
    {
        $this->client->setDefer(false);
    }

    public function getError()
    {
        return $this->error;
    }

    public function getBackups($client = null)
    {
        if ($client === null) {
            $client = $this->client;
        }

        $this->service = new GoogleDriveService($client);

        $filesStored = $this->service->files->listFiles([
            'q'       => "'" . $this->folderId . "' in parents",
            'fields'  => 'nextPageToken, files(id, name, mimeType)',
            'orderBy' => 'createdTime'
        ]);

        $files = $filesStored->getFiles();

        $backups = [];
        foreach ($files as $file) {
            if ($this->strings->endsWith($file->getName(), '.wpstg')) {
                $backups[] = $file;
            }
        }

        return $backups;
    }

    public function deleteOldestBackups()
    {
        $filesToDelete = $this->getBackups();

        if (count($filesToDelete) < $this->maxBackupsToKeep) {
            return true;
        }

        $filesToDeleteCount = count($filesToDelete) - $this->maxBackupsToKeep;

        for ($i = 0; $i <= $filesToDeleteCount; $i++) {
            $fileToDelete = $filesToDelete[$i];
            $this->service->files->delete($fileToDelete->id);
        }

        return true;
    }

    /**
     * @param GoogleDriveService $service
     * @return bool
     */
    private function checkDiskSpace($service = null)
    {
        if (apply_filters('wpstg.googleDrive.bypassDiskSpace', false)) {
            return true;
        }

        if ($service === null) {
            $service = $this->service;
        }

        try {
            $storage = $this->auth->getStorageInfo($service);
            $totalQuota = $storage->getLimit();
            $usedQuota = $storage->getUsage();
        } catch (Exception $ex) {
            return true;
        }

        if (!is_numeric($totalQuota) || !is_numeric($usedQuota)) {
            $this->logger->warning('Unable to get size of used or available storage space. Continuing with Upload to Google Drive!');
            return true;
        }

        $availableQuota = $totalQuota - $usedQuota;
        if (empty($availableQuota) || !is_numeric($availableQuota) || $availableQuota < 0) {
            return true;
        }

        if ($this->fileObject->getSize() > $availableQuota) {
            $this->error = sprintf(__('Google Drive Disk Quota Exceeded. Backup Size: %s. Space Available: %s. Backup is still available locally.', 'wp-staging'), size_format($this->fileObject->getSize(), 2), size_format($availableQuota, 2));
            return false;
        }

        return true;
    }
}
