<?php

defined("ABSPATH") or die("");

use Duplicator\Addons\GDriveAddon\Models\GDriveStorage;
use Duplicator\Models\DynamicGlobalEntity;
use Duplicator\Models\SystemGlobalEntity;
use Duplicator\Utils\Crypt\CryptBlowfish;
use VendorDuplicator\Google_Client;
use VendorDuplicator\Google_Exception;
use VendorDuplicator\Google_IO_Stream;
use VendorDuplicator\Google_Service_Oauth2;
use VendorDuplicator\Google_Service_Drive;
use VendorDuplicator\Google_Service_Drive_DriveFile;
use VendorDuplicator\Google_Service_Oauth2_Userinfoplus;

class DUP_PRO_GDriveClient_UploadInfo
{
    /** @var string */
    public $resume_uri = '';
    /** @var int */
    public $next_offset = 0;
    /** @var ?string */
    public $error_details = null;
    /** @var bool */
    public $is_complete = false;
}

class DUP_PRO_GDrive_U
{
    const RedirectUri = "https://snapcreek.com/misc/gdrive/auth_callback.php";

    const UploadChunkSizeBytes = 1048576; // 2097152;

    // These are requested scopes, possibly can be different than required scopes
    const SCOPES = array(
        "openid",
        "https://www.googleapis.com/auth/userinfo.profile",
        "https://www.googleapis.com/auth/userinfo.email",
        // The drive.file scope limits access to just those files created by the plugin
        "https://www.googleapis.com/auth/drive.file",
    );

    const REQUIRED_SCOPES = array(
        "openid",
        "https://www.googleapis.com/auth/userinfo.profile",
        "https://www.googleapis.com/auth/userinfo.email",
        // The drive.file scope limits access to just those files created by the plugin
        "https://www.googleapis.com/auth/drive.file",
    );

    /**
     * Get directory view link
     *
     * @param Google_Service_Drive $google_service_drive Google service drive
     * @param string               $directory            Directory
     *
     * @return string|null
     */
    public static function get_directory_view_link(Google_Service_Drive $google_service_drive, $directory)
    {
        $directory_id = DUP_PRO_GDrive_U::get_directory_id($google_service_drive, $directory);

        if ($directory_id != null) {
            $directory_metadata = DUP_PRO_GDrive_U::get_file_metadata_by_id($google_service_drive, $directory_id);

            if ($directory_metadata != null) {
                DUP_PRO_Log::trace("Directory link = " . $directory_metadata->alternateLink); // @phpstan-ignore-line

                return $directory_metadata->alternateLink; // @phpstan-ignore-line
            } else {
                DUP_PRO_Log::trace("Directory link for $directory not found");

                return null;
            }
        } else {
            DUP_PRO_Log::trace("Directory id for $directory not found");
            return null;
        }
    }

    /**
     * Get file metada
     *
     * @param Google_Service_Drive $google_service_drive Google service drive
     * @param string               $file_id              File id
     *
     * @return false|Google_Service_Drive_DriveFile false on failure
     */
    public static function get_file_metadata_by_id(Google_Service_Drive $google_service_drive, $file_id)
    {
        try {
            $file_metadata = $google_service_drive->files->get($file_id);
        } catch (Exception $ex) {
            DUP_PRO_Log::trace("Problems retrieving metadata for file $file_id");
            return false;
        }

        return $file_metadata;
    }

    /**
     * Delete file
     *
     * @param Google_Service_Drive $google_service_drive Google service drive
     * @param string               $file_id              File id
     *
     * @return bool
     */
    public static function delete_file(Google_Service_Drive $google_service_drive, $file_id)
    {
        $success = false;

        try {
            $google_service_drive->files->delete($file_id);
            $success = true;
            DUP_PRO_Log::trace("Delete of Google Drive file $file_id succeeded");
        } catch (Exception $ex) {
            DUP_PRO_Log::trace("Exception when trying to delete Google Drive file id $file_id");
        }

        return $success;
    }

    /**
     * Retrieve files in a given directory orderd by creation date
     *
     * @param Google_Service_Drive $google_service_drive Google service drive
     * @param string               $directory_id         Directory id
     *
     * @return null|object[]
     */
    public static function get_files_in_directory(Google_Service_Drive $google_service_drive, $directory_id)
    {
        $file_items = null;

        $parameters = array(
            'orderBy' => 'createdTime',
            'q'       => "'$directory_id' in parents and trashed=false",
        );

        try {
            $file_list  = $google_service_drive->files->listFiles($parameters);
            $file_items = $file_list->getFiles();
        } catch (\Exception $ex) {
            DUP_PRO_Log::trace("Error retrieving file list for directory ID $directory_id " . $ex->getMessage());
        }

        return $file_items;
    }

    /**
     * Get file
     *
     * @param Google_Service_Drive $google_service_drive Google service drive
     * @param string               $filename             Filename
     * @param string               $directory_id         Directory id
     *
     * @return null|string
     */
    public static function get_file(
        Google_Service_Drive $google_service_drive,
        $filename,
        $directory_id
    ) {
        DUP_PRO_Log::trace("get_file for $filename $directory_id");
        $file_id = null;

        $file_items = self::get_files_in_directory($google_service_drive, $directory_id);

        if ($file_items != null) {
            foreach ($file_items as $drive_file) {
                /* @var $drive_file Google_Service_Drive_DriveFile */

                $google_filename = $drive_file->getName();

                if ($google_filename == $filename) {
                    $file_id = $drive_file->getId();
                    break;
                }
            }
        } else {
            DUP_PRO_Log::trace("files in directory $directory_id are null");
        }

        return $file_id;
    }

    /**
     * Get directory id
     *
     * @param Google_Service_Drive $google_service_drive Google service drive
     * @param string               $path                 Path
     * @param bool                 $autocreate           Autocreate
     *
     * @return null|string
     */
    public static function get_directory_id(
        Google_Service_Drive $google_service_drive,
        $path,
        $autocreate = true
    ) {
        $parent_id = 'root';

        $path = str_replace('\\', '/', $path);
        $path = trim(trim($path), '/'); // Remove whitespaces and slashes from both ends of path
        $path = preg_replace('#/+#', '/', $path); // Replace all duplicated slashes with a single slash

        if ($path == "") {
            return $parent_id;
        }

        $directory_parts = explode('/', $path);

        try {
            foreach ($directory_parts as $subdirectory) {
                $parameters = array();

                $parameters['q'] = "'$parent_id' in parents and mimeType = 'application/vnd.google-apps.folder' and trashed=false";

                $file_list = $google_service_drive->files->listFiles($parameters);

                $folder_id = '';

                $items = $file_list->getFiles();

                foreach ($items as $drive_file) {
                    /* @var $drive_file Google_Service_Drive_DriveFile */
                    if ($drive_file->name == $subdirectory) {
                        $folder_id = $drive_file->id;
                        break;
                    } else {
                        DUP_PRO_Log::trace("{$drive_file->name} doesnt equal $subdirectory");
                    }
                }

                if ($folder_id == '') {
                    if ($autocreate) {
                        DUP_PRO_Log::trace("Creating new folder " . $subdirectory);

                        // Folder wasn't present so we have to create one
                        $folder_file = new Google_Service_Drive_DriveFile();
                        $folder_file->setName($subdirectory);
                        $folder_file->setMimeType('application/vnd.google-apps.folder');
                        $folder_file->setParents(array($parent_id));

                        $created_file = $google_service_drive->files->create($folder_file, array('mimeType' => 'application/vnd.google-apps.folder'));

                        $folder_id = $created_file->id;
                    } else {
                        // Doesn't exist
                        $parent_id = null;
                    }
                }

                $parent_id = $folder_id;
            }
        } catch (Exception $ex) {
            DUP_PRO_Log::trace("Got error when trying to get directory id for $path: " . $ex->getMessage());
            $parent_id = null;
        }

        return $parent_id;
    }

    /**
     * Upload file
     *
     * @param Google_Client $google_client  Google client
     * @param string        $src_file_path  Source file path
     * @param string        $parent_file_id Parent file id
     * @param string        $dest_file_name Destination file name
     *
     * @return null|Google_Service_Drive_DriveFile
     */
    public static function upload_file(
        Google_Client $google_client,
        $src_file_path,
        $parent_file_id,
        $dest_file_name = ''
    ) {
        /* @var $google_Client Google_Client */

        $drive_file = null;

        /* @var $google_service_drive Google_Service_Drive */
        try {
            $mime_type = 'application/octet-stream';

            $google_service_drive = new Google_Service_Drive($google_client);

            $upload_file = new Google_Service_Drive_DriveFile();
            if (empty($dest_file_name)) {
                $dest_file_name = basename($src_file_path);
            }
            $upload_file->setName($dest_file_name);

            $upload_file->setMimeType($mime_type);
            $upload_file->setParents(array($parent_file_id));

            try {
                $data = file_get_contents($src_file_path);

                if ($data !== false) {
                    //  DUP_PRO_Log::traceObject("file to upload", $upload_file)
                    /* @var $drive_file Google_Service_Drive_DriveFile */
                    $drive_file = $google_service_drive->files->create($upload_file, array('data' => $data, 'uploadType' => 'media'));
                } else {
                    DUP_PRO_Log::trace("Couldn't read file contents from $src_file_path when attempting Google Drive Upload");
                }
            } catch (Exception $ex) {
                DUP_PRO_Log::trace("Exception from Google drive insert of $src_file_path " . $ex->getMessage());
            }

            if (isset($drive_file) == false) {
                DUP_PRO_Log::trace("File returned from Google drive insert of $src_file_path is null.");
            }
        } catch (Exception $ex) {
            DUP_PRO_Log::trace("Error uploading $src_file_path to Google Drive");
        }

        return $drive_file;
    }

    /**
     * Upload file chunk
     *
     * @param Google_Client $google_client          Google client
     * @param string        $src_file_path          Source file path
     * @param string        $parent_file_id         Parent file id
     * @param int           $upload_chunk_size      Upload chunk size
     * @param int           $max_upload_time_in_sec Max upload time in sec
     * @param int           $next_offset            Next offset
     * @param string        $resume_uri             Resume uri
     * @param int           $server_load_delay      Server load delay
     *
     * @return DUP_PRO_GDriveClient_UploadInfo
     */
    public static function upload_file_chunk(
        Google_Client $google_client,
        $src_file_path,
        $parent_file_id,
        $upload_chunk_size = self::UploadChunkSizeBytes,
        $max_upload_time_in_sec = 10,
        $next_offset = 0,
        $resume_uri = null,
        $server_load_delay = 0
    ) {
        $upload_info = new DUP_PRO_GDriveClient_UploadInfo();

        try {
            if (file_exists($src_file_path) == false) {
                $upload_info->error_details = "$src_file_path doesn't exist!";
            }

            $google_service_drive = new Google_Service_Drive($google_client);

            $google_client->setDefer(true);

            $upload_file       = new Google_Service_Drive_DriveFile();
            $upload_file->name = basename($src_file_path);
            $upload_file->setMimeType('application/octet-stream');

            $upload_file->setParents(array($parent_file_id));

            $request = $google_service_drive->files->create($upload_file);

            if ($resume_uri == null) {
                $resume_uri = false;
            }

            $media_file_upload = new DUP_Pro_EnhancedGoogleMediaFileUpload(
                $google_client,
                $request,
                'binary/octet-stream',
                null,
                true,
                $upload_chunk_size,
                false,
                $next_offset,
                $resume_uri
            );

            $media_file_upload->setFileSize(filesize($src_file_path));

            // Upload the various chunks. $status will be false until the process is complete.
            $handle = fopen($src_file_path, "rb");

            if ($handle != false) {
                fseek($handle, $next_offset);

                $start_time  = time();
                $time_passed = 0;

                while (!$upload_info->is_complete && !feof($handle) && ($time_passed < $max_upload_time_in_sec)) {
                    if ($server_load_delay !== 0) {
                        usleep($server_load_delay);
                    }

                    $chunk = self::read_file_chunk($handle, $upload_chunk_size);

                    $upload_info->is_complete = ($media_file_upload->nextChunk($chunk) !== false);
                    $upload_info->resume_uri  = $media_file_upload->resumeUri;
                    $upload_info->next_offset = $media_file_upload->getNextOffset();

                    fseek($handle, $upload_info->next_offset);

                    $time_passed = time() - $start_time;
                }

                if ($upload_info->is_complete) {
                    DUP_PRO_Log::trace("Upload info is complete!");
                }

                fclose($handle);
            } else {
                $upload_info->error_details = "Error opening $src_file_path";
            }
        } catch (Exception $ex) {
            $message = $ex->getMessage();

            $upload_info->error_details = "Error uploading to Google Drive: " . $message;

            if (DUP_PRO_STR::contains($message, 'storage quota has been exceeded')) {
                $system_global = SystemGlobalEntity::getInstance();
                $system_global->addTextFix(
                    esc_html__('Google Drive out of storage space', "duplicator-pro"),
                    esc_html__('Free up space on Google Drive or increase storage quota.', "duplicator-pro")
                );
            }
        }

        $google_client->setDefer(false);

        return $upload_info;
    }

    /**
     * Download file
     *
     * @param Google_Client                  $google_client   Google client
     * @param Google_Service_Drive_DriveFile $google_file     Google file
     * @param string                         $local_filepath  Local filepath
     * @param bool                           $overwrite_local Overwrite local
     *
     * @return bool
     */
    public static function download_file(
        Google_Client $google_client,
        Google_Service_Drive_DriveFile $google_file,
        $local_filepath,
        $overwrite_local = true
    ) {
        $success = false;

        if ($overwrite_local || (file_exists($local_filepath) === false)) {
            $google_service_drive = new Google_Service_Drive($google_client);

            $file_contents = $google_service_drive->files->get($google_file->id, array('alt' => 'media'));

            if (@file_put_contents($local_filepath, $file_contents) === false) {
                DUP_PRO_Log::trace("Problem writing downloaded file to $local_filepath!");
            } else {
                $success = true;
            }
        } else {
            DUP_PRO_Log::trace("Attempted to download a file to $local_filepath but that file already exists!");
        }

        return $success;
    }

    /**
     * Read file chunk
     *
     * @param resource $handle     Handle
     * @param int      $chunk_size Chunk size
     *
     * @return string
     */
    public static function read_file_chunk($handle, $chunk_size)
    {
        $byte_count  = 0;
        $giant_chunk = "";

        while (!feof($handle)) {
            // fread will never return more than 8192 bytes if the stream is read buffered and it does not represent a plain file
            $chunk = fread($handle, 8192);

            $byte_count  += strlen($chunk);
            $giant_chunk .= $chunk;

            if ($byte_count >= $chunk_size) {
                return $giant_chunk;
            }
        }

        return $giant_chunk;
    }

    /**
     * Return user info
     *
     * @param Google_Client $google_client Google client
     *
     * @return ?Google_Service_Oauth2_Userinfoplus
     */
    public static function get_user_info(Google_Client $google_client)
    {
        $userInfoService = new Google_Service_Oauth2($google_client);
        $userInfo        = null;

        try {
            $userInfo = $userInfoService->userinfo->get();
        } catch (Google_Exception $e) {
            DUP_PRO_Log::trace("Error retrieving user information");
        }

        if ($userInfo->getId() == null) {
            $userInfo = null;
        }

        return $userInfo;
    }

    /**
     * Returns binary self value
     *
     * @return string
     */
    public static function get_binary_self_value()
    {
        return 'jfds2!x4';
    }

    /**
     * Returns binary extraction value
     *
     * @return string
     */
    public static function get_binary_extraction_value()
    {
        return 'kkd23p';
    }

    /**
     * Returns true if all required scopes are permitted, else returns false
     *
     * @param string $scopesToCheck Scopes to check, delimiter is space
     *
     * @return boolean
     */
    public static function checkScopes($scopesToCheck)
    {
        $scopesToCheck = preg_split('~\s+~', $scopesToCheck, -1, PREG_SPLIT_NO_EMPTY);
        if ($scopesToCheck == false) {
            return false;
        }
        $ok = true;
        foreach (self::REQUIRED_SCOPES as $requiredScope) {
            if (!in_array($requiredScope, $scopesToCheck)) {
                $ok = false;
                break;
            }
        }
        return $ok;
    }

    /**
     * Get google client
     *
     * @param int $gdrive_client_number Gdrive client number
     *
     * @return Google_Client
     */
    public static function get_raw_google_client($gdrive_client_number = null)
    {
        $global = DUP_PRO_Global_Entity::getInstance();
        $client = new Google_Client();

        if (DynamicGlobalEntity::getInstance()->getVal('gdrive_transfer_mode') == DUP_PRO_Google_Drive_Transfer_Mode::FOpen_URL) {
            $io = new Google_IO_Stream($client);
            $client->setIo($io);
        }

        $io      = $client->getIo();
        $setopts = array();

        if (is_a($io, 'Google_IO_Curl')) {
            $setopts[CURLOPT_SSL_VERIFYPEER] = $global->ssl_disableverify ? false : true;
            $setopts[CURLOPT_SSL_VERIFYHOST] = $global->ssl_disableverify ? 0 : 2;

            if (!$global->ssl_useservercerts) {
                $setopts[CURLOPT_CAINFO] = DUPLICATOR_PRO_CERT_PATH;
                $setopts[CURLOPT_CAPATH] = DUPLICATOR_PRO_CERT_PATH;
            }
            // Raise the timeout from the default of 15
            $setopts[CURLOPT_TIMEOUT]        = 360;
            $setopts[CURLOPT_CONNECTTIMEOUT] = 180;
            if ($global->ipv4_only) {
                $setopts[CURLOPT_IPRESOLVE] = CURL_IPRESOLVE_V4;
            }
        } elseif (is_a($io, 'Google_IO_Stream')) {
            $setopts['timeout'] = 360;
            // https://wiki.php.net/rfc/tls-peer-verification - before PHP 5.6, there is no default CA file
            if (!$global->ssl_useservercerts || (version_compare(PHP_VERSION, '5.6.0', '<'))) {
                $setopts['cafile'] = DUPLICATOR_PRO_CERT_PATH;
            }
            if ($global->ssl_disableverify) {
                $setopts['disable_verify_peer'] = true;
            }
        }
        $io->setOptions($setopts);

        $sv = self::get_binary_self_value();
        $ev = self::get_binary_extraction_value();

        $ci = CryptBlowfish::decrypt('EQNJ53++6/40fuF5ke+IaQ==', $sv);
        $cs = CryptBlowfish::decrypt('ui25chqoBexPt6QDi9qmGg==', $ev);

        $ci = trim($ci);
        $cs = trim($cs);

        switch ($gdrive_client_number) {
            case GDriveStorage::GDRIVE_CLIENT_NATIVE:
                if (($ci != $cs) || ($ci != "x93fdf8")) {
                    $ci = self::get_cj1() . self::get_cj2();
                    $cs = self::get_ct1() . self::get_ct2();
                }
                break;
            case GDriveStorage::GDRIVE_CLIENT_WEB0722:
            case GDriveStorage::GDRIVE_CLIENT_LATEST:
            default:
                if (($ci != $cs) || ($ci != "x93fdf8")) {
                    $ci = self::get_cj3() . self::get_cj4();
                    $cs = self::get_ct3() . self::get_ct4();
                }
        }

        $client->setClientId($ci);
        $client->setAccessType('offline');
        $client->setClientSecret($cs);
        $client->setScopes(self::SCOPES);
        $client->setRedirectUri(self::RedirectUri);

        return $client;
    }

    /**
     * Returns part of ci
     *
     * @return string
     */
    private static function get_cj1()
    {
        return base64_decode('MTMwOTA5MDkxOTkzLTZlMzFpNHN2cW9uaG9iMmRz');
    }

    /**
     * Returns part of ci
     *
     * @return string
     */
    private static function get_cj2()
    {
        return base64_decode('a2Zkc2R2cThvbWxnN3RlLmFwcHMuZ29vZ2xldXNlcmNvbnRlbnQuY29t');
    }

    /**
     * Returns part of ci
     *
     * @return string
     */
    private static function get_cj3()
    {
        return base64_decode('MTMwOTA5MDkxOTkzLWVwbWY2aHBjZnNmbDduZW5p');
    }

    /**
     * Returns part of ci
     *
     * @return string
     */
    private static function get_cj4()
    {
        return base64_decode('MzV0cjNjb2VvcXRvcDhjLmFwcHMuZ29vZ2xldXNlcmNvbnRlbnQuY29t');
    }

    /**
     * Returns part of cs
     *
     * @return string
     */
    private static function get_ct1()
    {
        return base64_decode('SVltaThQVnlzblFNbGo3');
    }

    /**
     * Returns part of cs
     *
     * @return string
     */
    private static function get_ct2()
    {
        return base64_decode('dHhuakgzN09t');
    }

    /**
     * Returns part of cs
     *
     * @return string
     */
    private static function get_ct3()
    {
        return base64_decode('R09DU1BYLWlIU3VZYnY1VHZa');
    }

    /**
     * Returns part of cs
     *
     * @return string
     */
    private static function get_ct4()
    {
        return base64_decode('SmJLVUIxa1VCclBIeG1JNGs=');
    }
}
