<?php
/**
 * @package   DPCalendar
 * @copyright Copyright (C) 2015 Digital Peak GmbH. <https://www.digital-peak.com>
 * @license   https://www.gnu.org/licenses/gpl-3.0.html GNU/GPL
 */

namespace DigitalPeak\Component\DPCalendar\Administrator\Plugin;

\defined('_JEXEC') or die();

use DigitalPeak\Component\DPCalendar\Administrator\Calendar\ExternalCalendar;
use DigitalPeak\Component\DPCalendar\Administrator\Calendar\ExternalCalendarInterface;
use DigitalPeak\Component\DPCalendar\Administrator\Helper\DPCalendarHelper;
use DigitalPeak\Component\DPCalendar\Administrator\Table\ExtcalendarTable;
use Joomla\CMS\Application\CMSApplicationInterface;
use Joomla\CMS\Date\Date;
use Joomla\Database\DatabaseAwareInterface;
use Joomla\Database\DatabaseAwareTrait;
use Joomla\Registry\Registry;

/**
 * This is the base class for the DPCalendar advanced sync plugins.
 */
abstract class SyncPlugin extends DPCalendarPlugin implements DatabaseAwareInterface
{
	use DatabaseAwareTrait;

	/**
	 * Getting the sync token to determine if a full sync needs to be done.
	 */
	protected function getSyncToken(ExternalCalendarInterface $calendar): string
	{
		$uri = str_replace('webcal://', 'https://', (string)$calendar->getParams()->get('uri'));

		if ($uri === '' || $uri === '0') {
			return (string)random_int(0, mt_getrandmax());
		}

		$internal = !filter_var($uri, FILTER_VALIDATE_URL);
		if ($internal && !str_starts_with($uri, '/')) {
			$uri = JPATH_ROOT . '/' . $uri;
		}

		if ($internal) {
			return (string)(filemtime($uri) ?: random_int(0, mt_getrandmax()));
		}

		$response = $this->getClientFactory()->create()->request($uri, '', null, null, [], [CURLOPT_NOBODY => true], 'head');

		if (\array_key_exists('etag', $response->dp->headers)) {
			return implode(', ', $response->dp->headers['etag']);
		}

		if (\array_key_exists('last-modified', $response->dp->headers)) {
			return implode(', ', $response->dp->headers['last-modified']);
		}

		return (string)random_int(0, mt_getrandmax());
	}

	/**
	 * Syncs the events of the given calendar.
	 * If the force flag is set, then the caching will be ignored.
	 */
	private function sync(ExternalCalendar $calendar, bool $force = false): void
	{
		$app = $this->getApplication();
		if (!$app instanceof CMSApplicationInterface) {
			return;
		}

		$component = $app->bootComponent('dpcalendar');

		$calendarId = str_replace($this->identifier . '-', '', $calendar->getId());
		$db         = $this->getDatabase();

		// Defining the last sync date
		$syncDate = $calendar->getSyncDate();
		if (!\in_array($syncDate, [null, '', '0'], true)) {
			$syncDate = DPCalendarHelper::getDate($syncDate);
		}

		// If the last sync is younger than the maximum cache time, return
		if (!$force && $syncDate && ($syncDate->format('U') + $this->params->get('cache_time', 900) >= DPCalendarHelper::getDate()->format('U'))) {
			return;
		}

		// Remove the script time limit.
		@set_time_limit(0);

		// Update the extcalendar table with the new sync information
		$extCalendarTable = $component->getMVCFactory()->createTable('Extcalendar', 'Administrator');
		$extCalendarTable->load(
			[
				'plugin' => $this->_name,
				'id'     => str_replace($this->identifier . '-', '', $calendar->getId())
			]
		);

		if (!$extCalendarTable->id) {
			return;
		}

		$extCalendarTable->sync_date = DPCalendarHelper::getDate()->toSql();
		$extCalendarTable->store();

		$this->extCalendarsCache = null;

		$syncToken = 1;
		if ($calendar->getSyncToken() !== null) {
			$syncToken = $this->getSyncToken($calendar);
			if ($syncToken === $calendar->getSyncToken()) {
				return;
			}
		}

		// Fetching the events to sync
		$syncDateStart = DPCalendarHelper::getDate();
		$syncDateStart->modify($this->params->get('sync_start', '-3 year'));

		// Defining the parameters
		$options = new Registry();
		$options->set('expand', false);

		$syncEnd = DPCalendarHelper::getDate();
		$syncEnd->modify($this->params->get('sync_end', '+3 year'));

		// If there are deleted events in the external calendar system we will detect them when publish down is set
		$db->setQuery('update #__dpcalendar_events set publish_down = now() where catid = ' . $db->quote($calendar->getId()));
		$db->execute();

		$foundEvents     = [];
		$processedEvents = [];
		while (true) {
			// Fetching in steps to safe memory
			$syncDateEnd = clone $syncDateStart;
			$syncDateEnd->modify('+' . $this->params->get('sync_steps', '1 year'));

			$events = $this->fetchEvents($calendarId, $options, $syncDateStart, $syncDateEnd);
			foreach ($events as $event) {
				// Check if we have processed the event already, mainly on recurring events
				if (\array_key_exists($event->id, $processedEvents)) {
					continue;
				}

				$processedEvents[$event->id] = $event;

				// Saving the id as reference
				$event->id    = null;
				$event->alias = null;

				// Find an existing event with the same keys
				$table = $component->getMVCFactory()->createTable('Event', 'Administrator');

				$keys = ['catid' => $calendar->getId(), 'uid' => $event->uid];
				if ($event->recurrence_id) {
					// Search the parent
					$table->load($keys);
					$event->original_id = $table->id;
					$table->reset();

					$keys['recurrence_id'] = $event->recurrence_id;
				}
				if ($event->original_id < 1) {
					$keys['original_id'] = $event->original_id;
				}
				$table->load($keys);

				// Check if the event was edited since last sync
				if ($syncDate && $event->modified && $syncDate->format('U') >= DPCalendarHelper::getDate($event->modified)->format('U')) {
					// Trashed events can be delivered separate
					if ($table->id && $table->state == $event->state) {
						$foundEvents[$table->id] = $table->id;
					}
					continue;
				}

				$event->id           = $table->id;
				$event->publish_down = '';

				$event->location_ids = [];
				foreach ($event->locations as $location) {
					$event->location_ids[$location->id] = $location->id;
				}

				// Save the event but do not trigger the event
				$model = $component->getMVCFactory()->createModel('Event', 'Administrator', ['ignore_request' => true, 'event_after_save' => 'ignore']);
				if (!$model->save((array)$event)) {
					$this->log($model->getError());
				}
			}

			$syncDateStart->modify('+' . $this->params->get('sync_steps', '1 year'));
			if ($syncDateEnd->format('U') > $syncEnd->format('U')) {
				break;
			}
		}

		if ($foundEvents !== []) {
			$db->setQuery(
				'update #__dpcalendar_events set publish_down = null where id in (' . implode(',', $foundEvents) . ') or original_id in (' . implode(',', $foundEvents) . ')'
			);
			$db->execute();
		}

		// Delete the events which are externally deleted
		$db->setQuery('delete from #__dpcalendar_events where catid = ' . $db->quote($calendar->getId()) . ' and publish_down is not null');
		$db->execute();

		$extCalendarTable->sync_date  = DPCalendarHelper::getDate()->toSql();
		$extCalendarTable->sync_token = (string)$syncToken;
		$extCalendarTable->store();
	}

	public function onEventsFetch(string $calendarId, ?Date $startDate = null, ?Date $endDate = null, ?Registry $options = null): array
	{
		if ($this->params->get('cache', 1) == 2) {
			return [];
		}

		return parent::onEventsFetch($calendarId, $startDate, $endDate, $options);
	}

	/**
	 * Function to force a sync.
	 */
	public function onEventsSync(?string $plugin = null, ?array $ids = []): void
	{
		// Only do a sync when enabled in the plugin
		if ($this->params->get('cache', 1) != 2) {
			return;
		}

		// If only a specific plugins needs to be synced return
		if ($plugin && $this->_name !== $plugin) {
			return;
		}

		// Loop through the calendars to sync
		foreach ($this->fetchCalendars() as $calendar) {
			if ($ids && !\in_array(str_replace($this->identifier . '-', '', (string)$calendar->getId()), $ids)) {
				continue;
			}
			$this->sync($calendar, true);
		}
	}

	public function onCalendarAfterDelete(ExtcalendarTable $calendar): void
	{
		if ($calendar->plugin !== $this->_name) {
			return;
		}

		// Clean the Joomla cache
		$cache = $this->getCacheControllerFactory()->createCacheController('callback', ['defaultgroup' => 'plg_dpcalendar_' . $calendar->plugin]);
		if (!$cache->clean()) {
			return;
		}

		$db = $this->getDatabase();
		$db->setQuery('delete from #__dpcalendar_events where catid = ' . $db->quote($this->identifier . '-' . $calendar->id));
		$db->execute();
	}
}
