<?php
/**
 * Akeeba Engine
 *
 * @package   akeebaengine
 * @copyright Copyright (c)2006-2023 Nicholas K. Dionysopoulos / Akeeba Ltd
 * @license   GNU General Public License version 3, or later
 */

namespace Akeeba\Engine\Util\Pushbullet;

defined('AKEEBAENGINE') || die();

use Akeeba\Engine\Postproc\ProxyAware;
use CURLFile;

/**
 * Based on Pushbullet-for-PHP 2.10.1 – https://github.com/ivkos/Pushbullet-for-PHP/tree/v2
 *
 * The license for the original class is as follows:
 * ----------
 * The MIT License (MIT)
 *
 * Copyright (c) 2014 Ivaylo Stoyanov
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 * ----------
 *
 * The following class is a derivative work, NOT the original work.
 */
class Connector
{
	use ProxyAware;

	public const URL_PUSHES = 'https://api.pushbullet.com/v2/pushes';
	public const URL_DEVICES = 'https://api.pushbullet.com/v2/devices';
	public const URL_CONTACTS = 'https://api.pushbullet.com/v2/contacts';
	public const URL_UPLOAD_REQUEST = 'https://api.pushbullet.com/v2/upload-request';
	public const URL_USERS = 'https://api.pushbullet.com/v2/users';
	public const URL_SUBSCRIPTIONS = 'https://api.pushbullet.com/v2/subscriptions';
	public const URL_CHANNEL_INFO = 'https://api.pushbullet.com/v2/channel-info';
	public const URL_EPHEMERALS = 'https://api.pushbullet.com/v2/ephemerals';
	public const URL_PHONEBOOK = 'https://api.pushbullet.com/v2/permanents/phonebook';
	private $_apiKey;
	private $_curlCallback;

	/**
	 * Pushbullet constructor.
	 *
	 * @param   string  $apiKey  API key.
	 *
	 * @throws ApiException
	 */
	public function __construct($apiKey)
	{
		$this->_apiKey = $apiKey;

		if (!function_exists('curl_init'))
		{
			throw new ApiException('cURL library is not loaded.');
		}
	}

	/**
	 * Parse recipient.
	 *
	 * @param   string  $recipient  Recipient string.
	 * @param   array   $data       Data array to populate with the correct recipient parameter.
	 */
	private static function _parseRecipient($recipient, array &$data)
	{
		if (!empty($recipient))
		{
			if (filter_var($recipient, FILTER_VALIDATE_EMAIL) !== false)
			{
				$data['email'] = $recipient;
			}
			else
			{
				if (substr($recipient, 0, 1) == "#")
				{
					$data['channel_tag'] = substr($recipient, 1);
				}
				else
				{
					$data['device_iden'] = $recipient;
				}
			}
		}
	}

	/**
	 * Push a note.
	 *
	 * @param   string  $recipient  Recipient. Can be device_iden, email or channel #tagname.
	 * @param   string  $title      The note's title.
	 * @param   string  $body       The note's message.
	 *
	 * @return object Response.
	 * @throws ApiException
	 */
	public function pushNote($recipient, $title, $body = null)
	{
		$data = [];

		Connector::_parseRecipient($recipient, $data);
		$data['type']  = 'note';
		$data['title'] = $title;
		$data['body']  = $body;

		return $this->_curlRequest(self::URL_PUSHES, 'POST', $data);
	}

	/**
	 * Push a link.
	 *
	 * @param   string  $recipient  Recipient. Can be device_iden, email or channel #tagname.
	 * @param   string  $title      The link's title.
	 * @param   string  $url        The URL to open.
	 * @param   string  $body       A message associated with the link.
	 *
	 * @return object Response.
	 * @throws ApiException
	 */
	public function pushLink($recipient, $title, $url, $body = null)
	{
		$data = [];

		Connector::_parseRecipient($recipient, $data);
		$data['type']  = 'link';
		$data['title'] = $title;
		$data['url']   = $url;
		$data['body']  = $body;

		return $this->_curlRequest(self::URL_PUSHES, 'POST', $data);
	}

	/**
	 * Push a checklist.
	 *
	 * @param   string    $recipient  Recipient. Can be device_iden, email or channel #tagname.
	 * @param   string    $title      The list's title.
	 * @param   string[]  $items      The list items.
	 *
	 * @return object Response.
	 * @throws ApiException
	 */
	public function pushList($recipient, $title, array $items)
	{
		$data = [];

		Connector::_parseRecipient($recipient, $data);
		$data['type']  = 'list';
		$data['title'] = $title;
		$data['items'] = $items;

		return $this->_curlRequest(self::URL_PUSHES, 'POST', $data);
	}

	/**
	 * Push a file.
	 *
	 * @param   string  $recipient    Recipient. Can be device_iden, email or channel #tagname.
	 * @param   string  $filePath     The path of the file to push.
	 * @param   string  $mimeType     The MIME type of the file. If null, we'll try to guess it.
	 * @param   string  $title        The title of the push notification.
	 * @param   string  $body         The body of the push notification.
	 * @param   string  $altFileName  Alternative file name to use instead of the original one.
	 *                                For example, you might want to push 'someFile.tmp' as 'image.jpg'.
	 *
	 * @return object Response.
	 * @throws ApiException
	 */
	public function pushFile($recipient, $filePath, $mimeType = null, $title = null, $body = null, $altFileName = null)
	{
		$data = [];

		$fullFilePath = realpath($filePath);

		if (!is_readable($fullFilePath))
		{
			throw new ApiException('File: File does not exist or is unreadable.');
		}

		if (filesize($fullFilePath) > 25 * 1024 * 1024)
		{
			throw new ApiException('File: File size exceeds 25 MB.');
		}

		$data['file_name'] = $altFileName ?? basename($fullFilePath);

		// Try to guess the MIME type if the argument is NULL
		$data['file_type'] = $mimeType ?? mime_content_type($fullFilePath);

		// Request authorization to upload the file
		$response         = $this->_curlRequest(self::URL_UPLOAD_REQUEST, 'GET', $data);
		$data['file_url'] = $response->file_url;

		$response->data->file = new CURLFile($fullFilePath);

		// Upload the file
		$this->_curlRequest($response->upload_url, 'POST', $response->data, false, false);

		Connector::_parseRecipient($recipient, $data);
		$data['type']  = 'file';
		$data['title'] = $title;
		$data['body']  = $body;

		return $this->_curlRequest(self::URL_PUSHES, 'POST', $data);
	}

	/**
	 * Get push history.
	 *
	 * @param   int     $modifiedAfter  Request pushes modified after this UNIX timestamp.
	 * @param   string  $cursor         Request the next page via its cursor from a previous response. See the API
	 *                                  documentation (https://docs.pushbullet.com/http/) for a detailed description.
	 * @param   int     $limit          Maximum number of objects on each page.
	 *
	 * @return object Response.
	 * @throws ApiException
	 */
	public function getPushHistory($modifiedAfter = 0, $cursor = null, $limit = null)
	{
		$data                   = [];
		$data['modified_after'] = $modifiedAfter;

		if ($cursor !== null)
		{
			$data['cursor'] = $cursor;
		}

		if ($limit !== null)
		{
			$data['limit'] = $limit;
		}

		return $this->_curlRequest(self::URL_PUSHES, 'GET', $data);
	}

	/**
	 * Dismiss a push.
	 *
	 * @param   string  $pushIden  push_iden of the push notification.
	 *
	 * @return object Response.
	 * @throws ApiException
	 */
	public function dismissPush($pushIden)
	{
		return $this->_curlRequest(self::URL_PUSHES . '/' . $pushIden, 'POST', ['dismissed' => true]);
	}

	/**
	 * Delete a push.
	 *
	 * @param   string  $pushIden  push_iden of the push notification.
	 *
	 * @return object Response.
	 * @throws ApiException
	 */
	public function deletePush($pushIden)
	{
		return $this->_curlRequest(self::URL_PUSHES . '/' . $pushIden, 'DELETE');
	}

	/**
	 * Get a list of available devices.
	 *
	 * @param   int     $modifiedAfter  Request devices modified after this UNIX timestamp.
	 * @param   string  $cursor         Request the next page via its cursor from a previous response. See the API
	 *                                  documentation (https://docs.pushbullet.com/http/) for a detailed description.
	 * @param   int     $limit          Maximum number of objects on each page.
	 *
	 * @return object Response.
	 * @throws ApiException
	 */
	public function getDevices($modifiedAfter = 0, $cursor = null, $limit = null)
	{
		$data                   = [];
		$data['modified_after'] = $modifiedAfter;

		if ($cursor !== null)
		{
			$data['cursor'] = $cursor;
		}

		if ($limit !== null)
		{
			$data['limit'] = $limit;
		}

		return $this->_curlRequest(self::URL_DEVICES, 'GET', $data);
	}

	/**
	 * Get information about the current user.
	 *
	 * @return object Response.
	 * @throws ApiException
	 */
	public function getUserInformation()
	{
		return $this->_curlRequest(self::URL_USERS . '/me', 'GET');
	}

	/**
	 * Update preferences for the current user.
	 *
	 * @param   array  $preferences  Preferences.
	 *
	 * @return object Response.
	 * @throws ApiException
	 */
	public function updateUserPreferences($preferences)
	{
		return $this->_curlRequest(self::URL_USERS . '/me', 'POST', ['preferences' => $preferences]);
	}

	/**
	 * Add a callback function that will be invoked right before executing each cURL request.
	 *
	 * @param   callable  $callback  The callback function.
	 */
	public function addCurlCallback(callable $callback)
	{
		$this->_curlCallback = $callback;
	}

	/**
	 * Send a request to a remote server using cURL.
	 *
	 * @param   string  $url         URL to send the request to.
	 * @param   string  $method      HTTP method.
	 * @param   array   $data        Query data.
	 * @param   bool    $sendAsJSON  Send the request as JSON.
	 * @param   bool    $auth        Use the API key to authenticate
	 *
	 * @return object Response.
	 * @throws ApiException
	 */
	private function _curlRequest($url, $method, $data = null, $sendAsJSON = true, $auth = true)
	{
		$curl = curl_init();

		$this->applyProxySettingsToCurl($curl);

		if ($method == 'GET' && $data !== null)
		{
			$url .= '?' . http_build_query($data);
		}

		curl_setopt($curl, CURLOPT_URL, $url);

		if ($auth)
		{
			curl_setopt($curl, CURLOPT_USERPWD, $this->_apiKey);
		}

		curl_setopt($curl, CURLOPT_CUSTOMREQUEST, $method);

		if ($method == 'POST' && $data !== null)
		{
			if ($sendAsJSON)
			{
				$data = json_encode($data);
				curl_setopt($curl, CURLOPT_HTTPHEADER, [
					'Content-Type: application/json',
					'Content-Length: ' . strlen($data),
				]);
			}

			curl_setopt($curl, CURLOPT_POSTFIELDS, $data);
		}

		curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
		curl_setopt($curl, CURLOPT_HEADER, false);

		@curl_setopt($curl, CURLOPT_CAINFO, AKEEBA_CACERT_PEM);
		curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, 2);
		curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, true);

		if ($this->_curlCallback !== null)
		{
			$curlCallback = $this->_curlCallback;
			$curlCallback($curl);
		}

		$response = curl_exec($curl);

		if ($response === false)
		{
			$curlError = curl_error($curl);
			curl_close($curl);
			throw new ApiException('cURL Error: ' . $curlError);
		}

		$httpCode = curl_getinfo($curl, CURLINFO_HTTP_CODE);

		if ($httpCode >= 400)
		{
			curl_close($curl);
			$responseParsed = json_decode($response);
			throw new ApiException('HTTP Error ' . $httpCode .
				' (' . $responseParsed->error->type . '): ' . $responseParsed->error->message);
		}

		curl_close($curl);

		return json_decode($response);
	}
}
