<?php
// phpcs:disable WordPress.WP.AlternativeFunctions.file_system_operations_fclose, WordPress.WP.AlternativeFunctions.file_system_operations_fopen, WordPress.WP.AlternativeFunctions.file_system_operations_fwrite, WordPress.WP.AlternativeFunctions.file_system_operations_fgets, WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents, WordPress.WP.AlternativeFunctions.file_system_operations_mkdir, WordPress.WP.AlternativeFunctions.file_system_operations_fread, WordPress.WP.AlternativeFunctions.file_system_operations_chmod, WordPress.WP.AlternativeFunctions.file_system_operations_fputs, WordPress.WP.AlternativeFunctions.file_system_operations_is_writeable, WordPress.WP.AlternativeFunctions.file_system_operations_chown, WordPress.WP.AlternativeFunctions.file_system_operations_chgrp, WordPress.WP.AlternativeFunctions.file_system_operations_touch -- Native PHP fileystem function is used for direct control and performance because it can bypass additional layers of abstraction so that no overhead from the WordPress filesystem API's internal handling
if (!defined('ABSPATH')) die('No direct access allowed');

/**
 * pCloud API implementation for UpdraftPlus
 *
 * @package PCloud
 */
class UpdraftPlus_Pcloud_API {

	/**
	 * Authentication token key
	 *
	 * @var string $auth Token key
	 */
	private $auth = '';

	/**
	 * Api Endpoint hostname
	 *
	 * @var string $apiep Api Endpoint
	 */
	private $apiep = '';

	/**
	 * Backup folder location
	 *
	 * @var string $folder Backup folder
	 */
	private $folder = '';

	/**
	 * The size in bytes of each uploaded/downloaded chunk
	 *
	 * @var int $part_size
	 */
	private $part_size = 2097152;

	/**
	 * Set authentication key
	 *
	 * @param string $auth Authentication token.
	 *
	 * @return void
	 */
	public function set_auth($auth = '') {
		$this->auth = $auth;
	}

	/**
	 * Set location ID
	 *
	 * @param int|string $location Location ID.
	 *
	 * @return void
	 */
	public function set_location($location = 1) {
		if (1 === $location) {
			$this->apiep = 'https://api.pcloud.com';
		} else {
			$this->apiep = 'https://eapi.pcloud.com';
		}
	}

	/**
	 * Set backup folder
	 *
	 * @param string $folder - the backup folder
	 *
	 * @return void
	 */
	public function set_folder($folder) {
		$this->folder = $folder;
	}

	/**
	 * Retrieves information about the user's account
	 *
	 * @return array|WP_Error - returns the response array or a WP_Error
	 */
	public function account_info() {
		
		$params = array(
			'access_token' => $this->auth
		);

		$args = array(
			'method' => 'GET',
		);

		$response = $this->make_request('userinfo', $params, $args);

		if (is_wp_error($response)) return $response;

		if (is_array($response) && isset($response['quota'])) {
			return $response;
		} else {
			return new WP_Error('unexpected_response', 'pCloud userinfo request returned an unexpected result: '.json_encode($response));
		}
	}

	/**
	 * Create base remote directory
	 *
	 * @param string $dir_name - the directory name.
	 *
	 * @return int|WP_Error - returns the int result or a WP_Error
	 */
	public function make_directory($dir_name) {

		$folder_id = 0;
		$folders = explode("/", $dir_name);

		foreach ($folders as $folder) {
			$params = array(
				'name' => untrailingslashit($folder),
				'folderid' => $folder_id,
				'access_token' => $this->auth,
			);
	
			$args = array(
				'method' => 'GET',
			);
	
			$response = $this->make_request('createfolderifnotexists', $params, $args);
	
			if (is_wp_error($response)) return $response;
	
			if (is_array($response) && isset($response['metadata']) && isset($response['metadata']['folderid'])) {
				$folder_id = intval($response['metadata']['folderid']);
			} else {
				return new WP_Error('unexpected_response', 'pCloud createfolderifnotexists request returned an unexpected result: '.json_encode($response));
			}
		}

		return $folder_id;
	}

	/**
	 * Get upload Dir ID
	 *
	 * @return int|WP_Error - returns upload folder ID or a WP_Error
	 */
	public function get_upload_dir_id() {

		$backup_dir = $this->get_backup_dir();

		$params = array(
			'path' => '/' . $backup_dir,
			'access_token' => $this->auth,
		);

		$args = array(
			'method' => 'GET',
		);

		$response = $this->make_request('listfolder', $params, $args);

		if (is_wp_error($response)) return $response;

		if (!isset($response['result']) || 2005 === $response['result']) {
			return $this->make_directory($backup_dir);
		} else {
			if (isset($response['metadata'])) {
				return $response['metadata']['folderid'];
			}
		}

		return 0;
	}

	/**
	 * Prepare to initiate Upload process
	 *
	 * @return array|WP_Error - returns response array or a WP_Error
	 */
	public function create_upload() {
		
		$params = array(
			'access_token' => $this->auth,
		);

		$args = array(
			'method' => 'GET',
		);

		$response = $this->make_request('upload_create', $params, $args);

		if (is_wp_error($response)) return $response;

		return $response;
	}

	/**
	 * Chunked upload
	 *
	 * @param string $path         - path/file to be uploaded.
	 * @param int    $upload_id    - pCloud Upload ID.
	 * @param int    $uploadoffset - file offset.
	 *
	 * @return int|WP_Error returns the new offset OR -2 when end of file reached or WP_Error
	 */
	public function chunked_upload($path, $upload_id = 0, $uploadoffset = 0) {

		if (!file_exists($path) || !is_file($path) || !is_readable($path)) {
			return new WP_Error('invalid_file', 'pCloud chunked upload: Invalid file provided: '.$path);
		}

		$filesize = abs(filesize($path));

		if ($uploadoffset >= $filesize) {
			return -2;
		}

		$file = fopen($path, 'r');

		if (0 < $uploadoffset) {
			fseek($file, $uploadoffset);
		}

		$params = array(
			'uploadid'     => $upload_id,
			'uploadoffset' => $uploadoffset,
		);

		$content = fread($file, $this->part_size);

		if (!empty($content)) {

			$result = $this->write($content, $params);

			if (is_wp_error($result)) {
				fclose($file);
				return $result;
			}

			$uploadoffset += $this->part_size;
		}

		fclose($file);

		if ($uploadoffset >= $filesize) {
			return -2;
		}

		return $uploadoffset;
	}

	/**
	 * Save the uploaded file
	 *
	 * @param int    $upload_id - pCloud Upload ID.
	 * @param string $path      - File Path.
	 * @param int    $folder_id - pCloud Folder ID.
	 *
	 * @return array|WP_Error - returns response array or a WP_Error
	 */
	public function save($upload_id, $path, $folder_id) {

		$path = str_replace(array('\\'), "/", $path);
		$parts = explode("/", $path);
		$filename = end($parts);

		$params = array(
			'uploadid' => intval($upload_id),
			'name' => rawurlencode($filename),
			'folderid' => intval($folder_id),
			'access_token' => $this->auth,
		);

		$args = array(
			'method' => 'GET',
		);

		$response = $this->make_request('upload_save', $params, $args);

		if (is_wp_error($response)) return $response;

		return $response;
	}

	/**
	 * Collecting existing backups on pCloud servers
	 *
	 * @return array|WP_Error - returns response array or a WP_Error
	 */
	public function list_backups() {

		$backup_dir = $this->get_backup_dir();

		$params = array(
			'path' => '/' . $backup_dir,
			'access_token' => $this->auth,
		);

		$args = array(
			'method' => 'GET',
		);

		$response = $this->make_request('listfolder', $params, $args);

		if (is_wp_error($response)) return $response;

		if (is_bool($response) || !isset($response['metadata']) || !isset($response['metadata']['contents'])) {
			return new WP_Error('unexpected_response', 'pCloud listfolder: returned an unexpected result: '.json_encode($response));
		}

		return $response['metadata']['contents'];
	}

	/**
	 * Chunked downloads a file.
	 *
	 * @param int      $file_id      - pCloud File ID.
	 * @param resource $archive_file - the local file handle.
	 * @param array    $options      - any extra options to be passed e.g. headers.
	 * @param array    $offset       - you can create real chunked download using this offset param.
	 *
	 * @return int
	 * @throws Exception Throws standard exception.
	 */
	public function download($file_id, $archive_file = null, $options = array(), $offset = 0) {

		$chunksize = 3 * (1024 * 1024);

		if (isset($options['Range'])) { // Disabled !
			$range_tmp = str_replace('bytes=', '', $options['Range']);
			$range_arr = explode('-', $range_tmp);
			if (is_array($range_arr)) {
				if (isset($range_arr[0]) && is_numeric($range_arr[0]) && isset($range_arr[1]) && is_numeric($range_arr[1])) {
					$offset = intval($range_arr[0]);
					$chunksize = intval($range_arr[1]) - $offset;
				}
			}
		}

		$dwl_url = '';

		$params = array(
			'fileid' => $file_id,
			'access_token' => $this->auth,
		);

		$args = array(
			'method' => 'GET',
		);

		$response = $this->make_request('getfilelink', $params, $args);

		if (is_wp_error($response)) return $response;
		
		if (isset($response['hosts']) && isset($response['hosts'][0])) {
			$dwl_url = $response['hosts'][0] . $response['path'];
		}

		if (empty($dwl_url)) {
			return new WP_Error('unexpected_response', 'pCloud download: Failed to get file download link.');
		}

		$errstr = '';

		$args = array(
			'headers' => array(
				'Range' => 'bytes=' . $offset . '-' . ($offset + ($chunksize - 1)),
			),
		);

		$content = false;
		$api_response = wp_remote_get('https://' . $dwl_url, $args);
		if (is_array($api_response) && !is_wp_error($api_response)) {
			$response_body = wp_remote_retrieve_body($api_response);
			if (is_string($response_body)) {
				$content = $response_body;
			}
		} else {
			$errstr = $api_response->get_error_message();
		}

		if (!$content) {
			return new WP_Error('unexpected_response', 'pCloud download: Failed to open connection to the backup file: ' . $dwl_url . ' error:' . $errstr);
		} else {
			if (!fwrite($archive_file, $content)) {
				return new WP_Error('unexpected_response', 'pCloud download: Failed to write content to output file.');
			} else {
				$offset += $chunksize;
			}
		}

		return $offset;
	}

	/**
	 * Deletes remote file
	 *
	 * @param string $path - The path to the file to be deleted.
	 *
	 * @return array|WP_Error - returns response array or a WP_Error
	 */
	public function delete($path) {

		$params = array(
			'path' => $path,
			'access_token' => $this->auth,
		);

		$args = array(
			'method' => 'GET',
		);

		$response = $this->make_request('deletefile', $params, $args);

		if (is_wp_error($response)) return $response;

		if (is_bool($response)) {
			return new WP_Error('unexpected_response', 'pCloud deletefile: returned an unexpected result: '.json_encode($response));
		}

		return $response;
	}

	/**
	 * Get File info from
	 *
	 * @param string $file - basename, needed file.
	 *
	 * @return array|WP_Error - returns response array or a WP_Error
	 */
	public function get_file_info($file) {

		$path = "/" . $this->get_backup_dir() . "/" . $file;

		$params = array(
			'path' => $path,
			'access_token' => $this->auth,
		);

		$args = array(
			'method' => 'GET',
		);

		$response = $this->make_request('checksumfile', $params, $args);

		if (is_wp_error($response)) return $response;

		if (is_bool($response)) {
			return new WP_Error('unexpected_response', 'pCloud checksumfile: returned an unexpected result: '.json_encode($response));
		}

		$response = $response['metadata'];

		return $response;
	}

	/**
	 * Get Backup directory
	 *
	 * @return string - the backup directory
	 */
	public function get_backup_dir() {
		return untrailingslashit(apply_filters('updraftplus_pcloud_backup_dir', 'UpdraftPlus').'/'.$this->folder);
	}

	/**
	 * Upload - write content chunk
	 *
	 * @param string $content - String content to be written.
	 * @param array  $params  - Additional request params.
	 *
	 * @return WP_Error - returns a WP_Error if something goes wrong
	 */
	private function write($content, $params) {

		$params['access_token'] = $this->auth;

		$args = array(
			'method'      => 'PUT',
			'redirection' => 5,
			'blocking'    => true,
			'headers'     => array(),
			'body'        => $content,
			'timeout'     => 60,
		);

		$response = $this->make_request('upload_write', $params, $args);

		if (is_wp_error($response)) return $response;
	}

	/**
	 * This function will make a API request andcheck the response. Returns the response or a WordPress error.
	 *
	 * @param string $endpoint - the API endpoint
	 * @param array  $params   - the API request parameters
	 * @param array  $args     - an array of request options
	 *
	 * @return array|WP_Error - returns an array response or WP_Error
	 */
	private function make_request($endpoint, $params, $args) {
		$api_response = wp_remote_request($this->apiep . '/'. $endpoint . '?' . http_build_query($params), $args);

		if (is_wp_error($api_response)) {
			return $api_response;
		}

		$response_code = wp_remote_retrieve_response_code($api_response);
		if ($response_code < 200 || $response_code >= 300) {
			return new WP_Error('unexpected_response_code', 'pCloud ' . $endpoint . ': returned an unexpected response code: '. $response_code . ' response: '.json_encode($api_response));
		}

		$response_body = wp_remote_retrieve_body($api_response);
		if (empty($response_body) || !is_string($response_body)) {
			return new WP_Error('no_response_body', 'pCloud ' . $endpoint . ': returned no response body: ' . json_encode($api_response));
		}
		
		$response_array = json_decode($response_body, true);
		if (!is_array($response_array)) {
			return new WP_Error('json_decode_failed', 'pCloud ' . $endpoint . ': Failed to json decode: ' . $response_body);
		}

		return $response_array;
	}
}
