<?php
// phpcs:disable WordPress.WP.AlternativeFunctions.file_system_operations_fwrite -- false positive; it's actually safe to use native PHP's fwrite()
// phpcs:disable WordPress.WP.AlternativeFunctions.file_system_operations_fclose -- using the native PHP fclose() function instead of the WP Filesystem API.
if (!defined('UPDRAFTPLUS_DIR')) 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,
		);

		$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;
	}
}
