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

namespace Akeeba\Component\AkeebaBackup\Administrator\Helper;

use Akeeba\Component\AkeebaBackup\Administrator\Model\PushModel;
use Akeeba\Engine\Platform;
use Akeeba\WebPush\NotificationOptions;
use Akeeba\WebPush\WebPush\MessageSentReport;
use Akeeba\WebPush\WebPush\WebPush;
use Joomla\CMS\Factory;
use Joomla\CMS\User\User;
use Joomla\CMS\User\UserFactoryInterface;
use Joomla\Database\DatabaseDriver;

/**
 * A replacement push notifications helper for the Akeeba Engine. This one is Web Push API aware.
 *
 * @since       9.3.1
 */
class PushMessages
{
	/**
	 * @since  9.3.1
	 * @var    \Joomla\CMS\MVC\Factory\MVCFactoryInterface
	 */
	public static $mvcFactory;

	/**
	 * User IDs who have Web Push enabled
	 *
	 * @since  9.3.1
	 * @var    array|null
	 */
	private static $webPushUsers = null;

	/**
	 * The classic push messages handler
	 *
	 * @since  9.3.1
	 * @var    \Akeeba\Engine\Util\PushMessages|null
	 */
	private $corePush = null;

	private $hasWebPush = false;

	/**
	 * Public constructor.
	 *
	 * Decides which push method to use.
	 *
	 * @since   9.3.1
	 */
	public function __construct()
	{
		$pushPreference   = Platform::getInstance()->get_platform_configuration_option('push_preference', '0');
		$this->hasWebPush = class_exists(WebPush::class);

		/**
		 * Fall back to the classic push handler if we're not using Web Push or if the integration is not available.
		 */
		if (!$this->hasWebPush || $pushPreference !== 'webpush')
		{
			$this->corePush = new \Akeeba\Engine\Util\PushMessages();
		}
	}

	/**
	 * Sends a push message, containing a URL/URI, to all connected devices. The URL will be rendered as something
	 * clickable on most devices.
	 *
	 * @param   string  $url      The URL/URI
	 * @param   string  $subject  The subject of the message, shown in the lock screen. Keep it short.
	 * @param   string  $details  Long(er) description of what the message is about. Plain text (no HTML).
	 *
	 * @return  void
	 * @since   9.3.1
	 */
	public function link($url, $subject, $details = null)
	{
		if (is_object($this->corePush))
		{
			$this->corePush->link($url, $subject, $details);

			return;
		}

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

		$details = $details ?? '';
		$details .= empty($details) ? '' : ' ';

		$this->message($subject, $details . $url);
	}

	/**
	 * Sends a push message to all connected devices. The intent is to provide the user with an information message,
	 * e.g. notify them about the progress of the backup.
	 *
	 * @param   string  $subject  The subject of the message, shown in the lock screen. Keep it short.
	 * @param   string  $details  Long(er) description of what the message is about. Plain text (no HTML).
	 *
	 * @return  void
	 * @since   9.3.1
	 */
	public function message($subject, $details = null)
	{
		if (is_object($this->corePush))
		{
			$this->corePush->message($subject, $details);

			return;
		}

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

		$logger = \Akeeba\Engine\Factory::getLog();

		$model = $this->getPushModel();

		if (empty($model))
		{
			$logger->notice('[Web Push] Cannot get the Web Push model or no push users are subscribed; aborted');

			return;
		}

		$options      = new NotificationOptions();
		$options->tag = 'com_akeebabackup';

		if ($details)
		{
			$options->body = $details;
		}

		foreach ($this->getWebPushUsers() as $uid)
		{
			$logger->debug(sprintf('[Web Push] Notifying user ID %d', $uid));

			try
			{
				$reports = $model->sendNotification($subject, $options->toArray(), $uid);
			}
			catch (\ErrorException $e)
			{
				$logger->notice(sprintf('[Web Push] PHP Error: %s', $e->getMessage()));
			}

			$failed = 0;
			$total  = 0;

			/** @var MessageSentReport $report */
			foreach ($reports as $report)
			{
				$total++;

				if (!$report->isSuccess())
				{
					$failed++;

					$logger->debug(sprintf('[Web Push] Partial failure: %s', $report->getReason()));
				}
			}

			if ($failed === $total)
			{
				$logger->debug(sprintf('[Web Push] Failed to notify user ID %s on %d subscription(s)', $uid, $total));
			}

			if ($failed > 0)
			{
				$logger->debug(sprintf('[Web Push] Partially notified user ID %d on %d subscription(s), %d of which failed', $uid, $total, $failed));
			}
			else
			{
				$logger->debug(sprintf('[Web Push] Successfully notified user ID %d on %d subscription(s)', $uid, $total));
			}
		}
	}

	/**
	 * Get the PushModel object, if possible
	 *
	 * @return  PushModel|null
	 *
	 * @since   9.3.1
	 */
	private function getPushModel(): ?PushModel
	{
		if (empty(self::$mvcFactory))
		{
			return null;
		}

		$userIDs = $this->getWebPushUsers();

		if (empty($userIDs))
		{
			return null;
		}

		try
		{
			/** @var PushModel|null $model */
			$model = self::$mvcFactory->createModel('Push', 'Administrator');
		}
		catch (\Throwable $e)
		{
			return null;
		}

		if (!is_object($model) || !method_exists($model, 'sendNotification'))
		{
			return null;
		}

		return $model;
	}

	/**
	 * Get the user IDs which should be receiving push messages.
	 *
	 * @return  int[]
	 *
	 * @since   9.3.1
	 */
	private function getWebPushUsers(): array
	{
		if (!is_null(self::$webPushUsers))
		{
			return self::$webPushUsers;
		}

		self::$webPushUsers = [];

		try
		{
			/** @var DatabaseDriver $db */
			$db    = Factory::getContainer()->get('DatabaseDriver');
			$query = $db->getQuery(true)
			            ->select($db->quoteName('user_id'))
			            ->from($db->quoteName('#__user_profiles'))
			            ->where($db->quoteName('profile_key') . ' = ' . $db->quote('com_akeebabackup.webPushSubscription'));

			self::$webPushUsers = array_filter(
				$db->setQuery($query)->loadColumn() ?: [],
				function ($uid) {
					/** @var User|null; $user */
					$user = Factory::getContainer()->get(UserFactoryInterface::class)->loadUserById($uid);

					return ($user instanceof User)
						&& $user->authorise('core.manage', 'com_akeebabackup');
				}
			);
		}
		catch (\Exception $e)
		{
			// Ignore errors. We just don't send push messages.
		}

		return self::$webPushUsers;
	}
}