403Webshell
Server IP : 80.87.202.40  /  Your IP : 216.73.216.169
Web Server : Apache
System : Linux rospirotorg.ru 5.14.0-539.el9.x86_64 #1 SMP PREEMPT_DYNAMIC Thu Dec 5 22:26:13 UTC 2024 x86_64
User : bitrix ( 600)
PHP Version : 8.2.27
Disable Function : NONE
MySQL : OFF |  cURL : ON |  WGET : ON |  Perl : ON |  Python : OFF |  Sudo : ON |  Pkexec : ON
Directory :  /home/bitrix/ext_www/rospirotorg.ru/bitrix/modules/calendar/classes/general/

Upload File :
current_dir [ Writeable] document_root [ Writeable]

 

Command :


[ Back ]     

Current File : /home/bitrix/ext_www/rospirotorg.ru/bitrix/modules/calendar/classes/general/calendar_event.php
<?php
IncludeModuleLangFile($_SERVER["DOCUMENT_ROOT"].BX_ROOT."/modules/calendar/classes/general/calendar.php");

use Bitrix\Calendar\Access\ActionDictionary;
use Bitrix\Calendar\Access\EventAccessController;
use Bitrix\Calendar\Access\Model\EventModel;
use Bitrix\Calendar\Core\Base\Date;
use Bitrix\Calendar\Core\Event\Event;
use Bitrix\Calendar\Core\Event\Properties\ExcludedDatesCollection;
use Bitrix\Calendar\Core\Event\Tools\Dictionary;
use Bitrix\Calendar\Core\Mappers;
use Bitrix\Calendar\Core\Mappers\Factory;
use Bitrix\Calendar\Core\Queue\Processor\SendingEmailNotification;
use Bitrix\Calendar\Core\Section\Section;
use Bitrix\Calendar\Integration\Bitrix24\FeatureDictionary;
use Bitrix\Calendar\Integration\Pull\PushCommand;
use Bitrix\Calendar\Integration\SocialNetwork\Collab;
use Bitrix\Calendar\Sharing;
use Bitrix\Calendar\Sync\Factories\FactoriesCollection;
use Bitrix\Calendar\Core\Event\Tools\UidGenerator;
use Bitrix\Calendar\Sync\Managers\Synchronization;
use Bitrix\Calendar\Sync\Util\Context;
use Bitrix\Calendar\UserSettings;
use Bitrix\Main\DI\ServiceLocator;
use Bitrix\Main\Entity\ReferenceField;
use Bitrix\Main\Localization\Loc;
use Bitrix\Main\ORM\Fields\Relations\Reference;
use Bitrix\Main\ORM\Query\Filter\ConditionTree;
use Bitrix\Main\ORM\Query\Join;
use Bitrix\Main\ORM\Query\Query;
use Bitrix\Main\Text\Emoji;
use Bitrix\Calendar\ICal\IncomingEventManager;
use Bitrix\Calendar\ICal\MailInvitation\InvitationInfo;
use Bitrix\Calendar\ICal\Basic\ICalUtil;
use Bitrix\Calendar\Internals;
use Bitrix\Calendar\Util;
use Bitrix\Calendar\Rooms;
use Bitrix\Disk\AttachedObject;
use Bitrix\Disk\Uf\FileUserType;
use Bitrix\Main\EventManager;
use Bitrix\Main\Loader;
use Bitrix\Main\UserTable;
use Bitrix\Calendar\Integration\Bitrix24Manager;
use Bitrix\Calendar\OpenEvents;

class CCalendarEvent
{
	public static
		$TextParser,
		$sendPush = true;

	private static
		$fields = [],
		$userIndex = [],
		$eventUserFields = [],
		$attendeeBelongingToEvent = [],
		$collabIdByParent = [],
		$isAddIcalFailEmailError = false,
		/** @var $openEventSection Section|null|false */
		$openEventSection = false,
		$getListAccessCheck = []
	;

	public static $defaultSelectEvent = [
		'ID',
		'PARENT_ID',
		'CREATED_BY',
		'OWNER_ID',
		'EVENT_TYPE',
		'NAME',
		'DATE_FROM',
		'DATE_TO',
		'TZ_FROM',
		'TZ_TO',
		'TZ_OFFSET_FROM',
		'TZ_OFFSET_TO',
		'DATE_FROM_TS_UTC',
		'DATE_TO_TS_UTC',
		'DT_SKIP_TIME',
		'ACCESSIBILITY',
		'IMPORTANCE',
		'RRULE',
		'EXDATE',
		'SECTION_ID',
		'CAL_TYPE',
		'MEETING_STATUS',
		'MEETING_HOST',
		'IS_MEETING',
		'DT_LENGTH',
		'PRIVATE_EVENT',
	];

	public static function CheckRRULE($recRule = [])
	{
		if (is_array($recRule) && isset($recRule['FREQ']) && $recRule['FREQ'] !== 'WEEKLY' && isset($recRule['BYDAY']))
		{
			unset($recRule['BYDAY']);
		}

		return $recRule;
	}

	/**
	 * @param array $params
	 * params['arFields'] event fields
	 * params['userId'] user id
	 * params['saveAttendeesStatus'] sending notification flag
	 *
	 * @return bool|mixed
	 *
	 * @throws \Bitrix\Main\ArgumentException
	 * @throws \Bitrix\Main\ArgumentNullException
	 * @throws \Bitrix\Main\ArgumentOutOfRangeException
	 * @throws \Bitrix\Main\LoaderException
	 * @throws \Bitrix\Main\NotImplementedException
	 * @throws \Bitrix\Main\ObjectException
	 * @throws \Bitrix\Main\ObjectPropertyException
	 * @throws \Bitrix\Main\SystemException
	 * @throws \Bitrix\Calendar\Rooms\OccupancyCheckerException
	 */
	public static function Edit($params = [])
	{
		global $DB, $CACHE_MANAGER;
		$entryFields = $params['arFields'] ?? [];
		$arAffectedSections = [];
		$entryChanges = [];
		$changedFields = [];
		$sendInvitations = ($params['sendInvitations'] ?? null) !== false;
		$sendEditNotification = ($params['sendEditNotification'] ?? null) !== false;
		$checkLocationOccupancy = ($params['checkLocationOccupancy'] ?? null) === true;
		$result = false;

		// Get current user id
		$userId = (isset($params['userId']) && (int)$params['userId'] > 0)
			? (int)$params['userId']
			: CCalendar::GetCurUserId();

		if (!$userId && isset($entryFields['CREATED_BY']))
		{
			$userId = (int)$entryFields['CREATED_BY'];
		}

		$isNewEvent = !isset($entryFields['ID']) || !$entryFields['ID'];
		$entryFields['TIMESTAMP_X'] = CCalendar::Date(time(), true, false);

		// Current event
		$currentEvent = [];
		if (!empty($entryFields['IS_MEETING']) && !isset($entryFields['ATTENDEES']) && isset($entryFields['ATTENDEES_CODES']))
		{
			$entryFields['ATTENDEES'] = \CCalendar::getDestinationUsers($entryFields['ATTENDEES_CODES']);
		}

		if (!$isNewEvent)
		{
			$currentEvent = $params['currentEvent'] ?? self::GetById($entryFields['ID'], $params['checkPermission'] ?? true);

			if (!isset($entryFields['LOCATION']) || !is_array($entryFields['LOCATION']))
			{
				$entryFields['LOCATION'] = [
					'NEW' => $entryFields['LOCATION'] ?? null,
				];
			}

			if (
				isset($entryFields['MEETING'])
				&& is_array($entryFields['MEETING'])
				&& is_array($currentEvent['MEETING'])
                && !isset($entryFields['MEETING']['CHAT_ID'])
                && isset($currentEvent['MEETING']['CHAT_ID'])
			)
			{
				$entryFields['MEETING']['CHAT_ID'] = $currentEvent['MEETING']['CHAT_ID'];
			}

			if (empty($entryFields['LOCATION']['OLD']))
			{
				$entryFields['LOCATION']['OLD'] = $currentEvent['LOCATION'] ?? null;
			}

			if (
				!empty($currentEvent['IS_MEETING'])
				&& !isset($entryFields['ATTENDEES'])
				&& $currentEvent['PARENT_ID'] === $currentEvent['ID']
				&& !empty($entryFields['IS_MEETING'])
			)
			{
				$entryFields['ATTENDEES'] = [];
				$attendees = self::GetAttendees($currentEvent['PARENT_ID']);
				if (!empty($attendees[$currentEvent['PARENT_ID']]))
				{
					foreach ($attendees[$currentEvent['PARENT_ID']] as $attendee)
					{
						$entryFields['ATTENDEES'][] = $attendee['USER_ID'];
					}
				}
			}

			if (!empty($currentEvent['PARENT_ID']))
			{
				$entryFields['PARENT_ID'] = (int)$currentEvent['PARENT_ID'];
			}
		}

		if (self::CheckFields($entryFields, $currentEvent, $userId))
		{
			$attendees = (isset($entryFields['ATTENDEES']) && is_array($entryFields['ATTENDEES']))
                ? $entryFields['ATTENDEES']
                : [];

			if (
                ($entryFields['CAL_TYPE'] ?? null) !== Rooms\Manager::TYPE
				&& (empty($entryFields['PARENT_ID']) || $entryFields['PARENT_ID'] === $entryFields['ID'])
			)
			{
				$fromTs = $entryFields['DATE_FROM_TS_UTC'] ?? null;
				$toTs = $entryFields['DATE_TO_TS_UTC'] ?? null;
				if (($entryFields['DT_SKIP_TIME'] ?? null) !== "Y")
				{
					$fromTs += (int)date('Z', $fromTs);
					$toTs += (int)date('Z', $toTs);
				}

				$entryFields['LOCATION'] = self::checkLocationField($entryFields['LOCATION'] ?? null, $isNewEvent);

				if ($checkLocationOccupancy)
				{
					self::checkLocationOccupancy($entryFields, $params, $currentEvent, $userId);
				}

				$entryFields['LOCATION'] = Bitrix\Calendar\Rooms\Util::setLocation(
					$entryFields['LOCATION']['OLD'],
					$entryFields['LOCATION']['NEW'],
					[
						// UTC timestamp + date('Z', $timestamp) /*offset of the server*/
						'dateFrom' => CCalendar::Date($fromTs, $entryFields['DT_SKIP_TIME'] !== "Y"),
						'dateTo' => CCalendar::Date($toTs, $entryFields['DT_SKIP_TIME'] !== "Y"),
						'parentParams' => $params,
						'name' => $entryFields['NAME'],
						'persons' => count($attendees),
						'attendees' => $attendees,
						'bRecreateReserveMeetings' => ($entryFields['LOCATION']['RE_RESERVE'] ?? null) !== 'N',
						'checkPermission' => $params['checkPermission'] ?? null,
					]
				);
			}
			else
			{
				$entryFields['LOCATION'] = self::checkLocationField($entryFields['LOCATION'], $isNewEvent);
				$entryFields['LOCATION'] = $entryFields['LOCATION']['NEW'];
			}

			// Section
			if (isset($entryFields['SECTION_ID']))
			{
				$sectionId = (int)$entryFields['SECTION_ID'];
			}
			else
			{
				$sectionId = !empty($entryFields['SECTIONS'][0])
                    ? (int)$entryFields['SECTIONS'][0]
					: false;
			}

			if (!$sectionId)
			{
				$sectionId = self::tryingToFindEventSection($isNewEvent, $entryFields, $userId, $currentEvent);
			}

			$entryFields['SECTION_ID'] = $sectionId;
			$arAffectedSections[] = $sectionId;

			$section = CCalendarSect::GetList(['arFilter' => ['ID' => $sectionId],
				'checkPermissions' => false,
				'getPermissions' => false,
			])[0] ?? null;

			// Here we take type and owner parameters from section data
			if ($section)
			{
				$entryFields['CAL_TYPE'] = $section['CAL_TYPE'];
				$entryFields['OWNER_ID'] = $section['OWNER_ID'] ?? '';

				if (
					$section['CAL_TYPE'] === Dictionary::CALENDAR_TYPE['group']
					&& Collab\Entity\SectionEntityHelper::getIfCollab($sectionId)
					&& $entryFields['EVENT_TYPE'] !== Dictionary::EVENT_TYPE['shared_collab']
				)
				{
					$entryFields['EVENT_TYPE'] = Dictionary::EVENT_TYPE['collab'];
				}
			}

			if (($entryFields['CAL_TYPE'] ?? null) === 'user')
			{
				$CACHE_MANAGER->ClearByTag('calendar_user_'.$entryFields['OWNER_ID']);
			}

			if ($isNewEvent)
			{
				if (!isset($entryFields['CREATED_BY']))
				{
					$entryFields['CREATED_BY'] = (
                        !empty($entryFields['IS_MEETING'])
						&& ($entryFields['CAL_TYPE'] ?? null) === 'user'
						&& !empty($entryFields['OWNER_ID'])
                    ) ? $entryFields['OWNER_ID'] : $userId;
				}

				if (!isset($entryFields['DATE_CREATE']))
				{
					$entryFields['DATE_CREATE'] = $entryFields['TIMESTAMP_X'];
				}
			}
			else
			{
				$arAffectedSections[] = $currentEvent['SECTION_ID'] ?? $currentEvent['SECT_ID'];
			}

			if (
				!isset($entryFields['IS_MEETING'])
				&& isset($entryFields['ATTENDEES'])
				&& is_array($entryFields['ATTENDEES'])
			&& empty($entryFields['ATTENDEES'])
			)
			{
				$entryFields['IS_MEETING'] = false;
			}
			if (!empty($entryFields['IS_MEETING']) && !$isNewEvent)
			{
				$entryChanges = self::CheckEntryChanges($entryFields, $currentEvent);
				$changedFields = array_column($entryChanges, 'fieldKey');
			}
			else if (
				!empty($entryFields['IS_MEETING'])
				&& !empty($params['editInstance'])
				&& !empty($params['instanceChanges'])
			)
			{
				$entryChanges = $params['instanceChanges'];
				$changedFields = array_column($entryChanges, 'fieldKey');
			}

			$attendeesCodes = $entryFields['ATTENDEES_CODES'] ?? null;
			if (is_array($attendeesCodes))
			{
				$entryFields['ATTENDEES_CODES'] = implode(',', $attendeesCodes);
			}

			if ($entryFields['CAL_TYPE'] === Dictionary::CALENDAR_TYPE['open_event'])
			{
				// for open events meeting status stored for each attendee in event_attendee table
				$entryFields['MEETING_STATUS'] = 'N';
			}
			else if (
                !isset($entryFields['MEETING_STATUS'])
				&& !empty($entryFields['MEETING_HOST'])
                && (int)$entryFields['MEETING_HOST'] === (int)($entryFields['CREATED_BY'] ?? null)
			)
			{
				$entryFields['MEETING_STATUS'] = 'H';
			}
			else if (!isset($entryFields['MEETING_STATUS']) && !$currentEvent)
			{
				$entryFields['MEETING_STATUS'] = 'Y';
			}

			if (isset($entryFields['MEETING']) && is_array($entryFields['MEETING']))
			{
				$entryFields['~MEETING'] = $entryFields['MEETING'];
				$entryFields['MEETING']['REINVITE'] = false;
				$meetingHostSettings = UserSettings::get($entryFields['MEETING_HOST'] ?? null);
				$entryFields['MEETING']['MAIL_FROM'] =
					$entryFields['MEETING']['MAIL_FROM']
					?? $meetingHostSettings['sendFromEmail']
					?? null
				;
				$entryFields['MEETING'] = serialize($entryFields['MEETING']);
			}

			if (isset($entryFields['RELATIONS']) && is_array($entryFields['RELATIONS']))
			{
				$entryFields['~RELATIONS'] = $entryFields['RELATIONS'];
				$entryFields['RELATIONS'] = serialize($entryFields['RELATIONS']);
			}

			if (
				isset($entryFields['REMIND'])
				&& (
					$isNewEvent
					|| !$entryFields['IS_MEETING']
					|| (int)$entryFields['CREATED_BY'] === $userId
					|| ($params['updateReminders'] ?? null) === true
				)
			)
			{
				$reminderList = CCalendarReminder::prepareReminder($entryFields['REMIND']);
			}
			elseif (!empty($currentEvent['REMIND']))
			{
				$reminderList = CCalendarReminder::prepareReminder($currentEvent['REMIND']);
			}
			else
			{
				$reminderList = [];
			}
			$entryFields['REMIND'] = serialize($reminderList);

			if (
				isset($entryFields['SYNC_STATUS'])
				&& !in_array($entryFields['SYNC_STATUS'],Bitrix\Calendar\Sync\Google\Dictionary::SYNC_STATUS, true)
			)
			{
				$entryFields['SYNC_STATUS'] = null;
			}

			if (isset($entryFields['EXDATE']) && is_array($entryFields['EXDATE']))
			{
				$entryFields['EXDATE'] = implode(';', $entryFields['EXDATE']);
			}
			$entryFields['EXDATE'] = !empty($entryFields['EXDATE'])
				? self::convertExDatesToInternalFormat($entryFields['EXDATE'])
				: ''
			;

			$entryFields['RRULE'] = self::convertRuleUntilToInternalFormat($entryFields['RRULE'] ?? null);

			$AllFields = self::GetFields();
			$dbFields = [];

			foreach($entryFields as $field => $val)
			{
				if (
					isset($AllFields[$field])
					&& $field !== "ID"
					&& is_scalar($val)
				)
				{
					$dbFields[$field] = $val;
				}
			}

			if (!empty($dbFields['NAME']))
			{
				$dbFields['NAME'] = Emoji::encode($dbFields['NAME']);
			}
			if (!empty($dbFields['DESCRIPTION']))
			{
				$dbFields['DESCRIPTION'] = Emoji::encode($dbFields['DESCRIPTION']);
			}
			if (!empty($dbFields['LOCATION']))
			{
				$dbFields['LOCATION'] = Emoji::encode($dbFields['LOCATION']);
			}

			CTimeZone::Disable();

			$isOpenEvent = ($entryFields['CAL_TYPE'] ?? null) === Dictionary::CALENDAR_TYPE['open_event'];

			if ($isNewEvent) // Add
			{
				$eventId = $DB->Add("b_calendar_event", $dbFields, ['DESCRIPTION', 'MEETING', 'EXDATE']);
			}
			else // Update
			{
				$eventId = $entryFields['ID'];
				$strUpdate = $DB->PrepareUpdate("b_calendar_event", $dbFields);
				$strSql =
					"UPDATE b_calendar_event SET ".
						$strUpdate.
						" WHERE ID=". (int)$eventId;

				$DB->QueryBind($strSql, array(
					'DESCRIPTION' => Emoji::encode($entryFields['DESCRIPTION'] ?? ''),
					'MEETING' => $entryFields['MEETING'] ?? null,
					'EXDATE' => $entryFields['EXDATE'] ?? null,
				));
				// update separated attendee fields
				if ($isOpenEvent)
				{
					self::updateEventAttendee($eventId, $userId, $changedFields, $dbFields);
				}
			}

			CTimeZone::Enable();

			if (
				$userId
				&& $params
				&& ($params['overSaving'] ?? null) !== true
				&& \Bitrix\Calendar\Sync\Util\RequestLogger::isEnabled()
			)
			{
				$loggerParams = $params;
				$loggerParams['arFields'] = $entryFields;
				$loggerParams['loggerUuid'] = $eventId;

				(new \Bitrix\Calendar\Sync\Util\RequestLogger($userId, 'portal_edit'))->write($loggerParams);
			}

			/**
			 * @deprecated
			 */
//			if ($isNewEvent && !isset($dbFields['DAV_XML_ID']))
//			{
//				$strSql =
//					"UPDATE b_calendar_event SET ".
//						$DB->PrepareUpdate("b_calendar_event", ['DAV_XML_ID' => (int)$eventId]).
//						" WHERE ID = ". (int)$eventId;
//				$DB->Query($strSql);
//			}
			/**
			 * @deprecated Now connection saved in the table
			 */
//			if (
//				!Util::isSectionStructureConverted()
//				&& ($isNewEvent || $sectionId !== $currentEvent['SECTION_ID']))
//			{
//				self::ConnectEventToSection($eventId, $sectionId);
//			}

			if (!empty($arAffectedSections))
			{
				CCalendarSect::UpdateModificationLabel($arAffectedSections);
			}

			if (
				!empty($entryFields['IS_MEETING'])
				|| (!$isNewEvent && !empty($currentEvent['IS_MEETING']))
			)
			{
				if (empty($entryFields['PARENT_ID']))
				{
					Internals\EventTable::update((int)$eventId, [
						'PARENT_ID' => (int)$eventId,
					]);
				}

				if (
					(empty($entryFields['PARENT_ID']) || $entryFields['PARENT_ID'] === $eventId)
					&& !$isOpenEvent
				)
				{
					self::CreateChildEvents($eventId, $entryFields, $params, $entryChanges);
				}

				if ((empty($entryFields['PARENT_ID']) || $entryFields['PARENT_ID'] === $eventId) && !empty($entryFields['RECURRENCE_ID']))
				{
					self::UpdateParentEventExDate($entryFields['RECURRENCE_ID'], $entryFields['ORIGINAL_DATE_FROM'], $entryFields['ATTENDEES']);
				}

				if (empty($entryFields['PARENT_ID']))
				{
					$entryFields['PARENT_ID'] = (int)$eventId;
				}
			}
			else if (($isNewEvent && empty($entryFields['PARENT_ID'])) || (!$isNewEvent && empty($currentEvent['PARENT_ID'])))
			{
				Internals\EventTable::update((int)$eventId, [
					'PARENT_ID' => (int)$eventId,
				]);

				if (empty($entryFields['PARENT_ID']))
				{
					$entryFields['PARENT_ID'] = (int)$eventId;
				}
			}

			// Update reminders for event
			CCalendarReminder::updateReminders([
				'id' => $eventId,
				'reminders' => $reminderList,
				'arFields' => $entryFields,
				'userId' => $userId,
			]);

			if ((empty($entryFields['PARENT_ID']) || $entryFields['PARENT_ID'] === $eventId))
			{
				self::updateSearchIndex((int)$eventId, [
					'userId' => $userId,
					'updateAllByParent' => true,
					'isNew' => $isNewEvent,
					'changedFields' => $changedFields,
				]);
			}

			$nowUtc = time() - date('Z');
			// Send invitations and notifications
			if (
				!empty($entryFields['IS_MEETING'])
				&& ($params['overSaving'] ?? null) !== true
			)
			{
				$fromTo = self::GetEventFromToForUser($entryFields, $entryFields['OWNER_ID'] ?? null);

				// If it's event in the past we're skipping notifications.
				// The past is the past...
				if (isset($entryFields['DATE_TO_TS_UTC']) && $entryFields['DATE_TO_TS_UTC'] > $nowUtc)
				{
					if (
						$sendEditNotification
						&& (int)($entryFields['PARENT_ID'] ?? null) !== (int)$eventId
						&& !empty($entryChanges)
						&& (
							($entryFields['MEETING_STATUS'] ?? null) === 'Y'
							|| ($entryFields['MEETING_STATUS'] ?? null) === 'H'
						)
					)
					{
						// third problematic place

						if (
							(!empty($entryFields['MEETING_HOST']) && (int)$entryFields['MEETING_HOST'] === (int)$userId)
							|| self::checkAttendeeBelongsToEvent($entryFields['PARENT_ID'] ?? null, $userId)
						)
						{
							$CACHE_MANAGER->ClearByTag('calendar_user_'.$entryFields['OWNER_ID']);
							CCalendarNotify::Send([
								'mode' => 'change_notify',
								'name' => $entryFields['NAME'] ?? null,
								"from" => $fromTo['DATE_FROM'] ?? null,
								"to" => $fromTo['DATE_TO'] ?? null,
								"location" => CCalendar::GetTextLocation($entryFields["LOCATION"] ?? null),
								"guestId" => $entryFields['OWNER_ID'] ?? null,
								"eventId" => $entryFields['PARENT_ID'] ?? null,
								"userId" => \CCalendar::GetUserId(),
								"fields" => $entryFields,
								"isSharing" => ($entryFields['EVENT_TYPE'] ?? null) === Dictionary::EVENT_TYPE['shared'],
								"entryChanges" => $entryChanges,
							]);
						}
					}
					elseif (
						(int)($entryFields['PARENT_ID'] ?? null) !== $eventId
						&& ($entryFields['MEETING_STATUS'] ?? null) === 'Q'
						&& $sendInvitations
					)
					{
						$CACHE_MANAGER->ClearByTag('calendar_user_'.$entryFields['OWNER_ID'] ?? '');
						CCalendarNotify::Send(array(
							"mode" => 'invite',
							"name" => $entryFields['NAME'] ?? null,
							"from" => $fromTo['DATE_FROM'] ?? null,
							"to" => $fromTo['DATE_TO'] ?? null,
							"location" => CCalendar::GetTextLocation($entryFields["LOCATION"] ?? null),
							"guestId" => $entryFields['OWNER_ID'] ?? null,
							"eventId" => $entryFields['PARENT_ID'] ?? null,
							"userId" => $entryFields['MEETING_HOST'] ?? \CCalendar::GetUserId(),
							"isSharing" => ($entryFields['EVENT_TYPE'] ?? null) === Dictionary::EVENT_TYPE['shared'],
							"fields" => $entryFields,
						));
					}
				}
			}

			if (
				!empty($entryFields['IS_MEETING'])
				&& !empty($entryFields['ATTENDEES_CODES'])
				&& (int)($entryFields['PARENT_ID'] ?? null) === (int)$eventId
				&& ($params['overSaving'] ?? null) !== true
				&& isset($entryFields['DATE_TO_TS_UTC'])
				&& $entryFields['DATE_TO_TS_UTC'] > $nowUtc
			)
			{
				CCalendarLiveFeed::OnEditCalendarEventEntry([
					'eventId' => $eventId,
					'arFields' => $entryFields,
					'attendeesCodes' => $attendeesCodes,
				]);
			}

			CCalendar::ClearCache('event_list');

			if (($entryFields['ACCESSIBILITY'] ?? '') === 'absent')
			{
				(new \Bitrix\Calendar\Integration\Intranet\Absence())->cleanCache();
			}

			$result = $eventId;

			if (!empty($entryFields['LOCATION']))
			{
				Rooms\Manager::setEventIdForLocation($eventId, $entryFields['LOCATION']);
			}

			if ($isNewEvent)
			{
				foreach(EventManager::getInstance()->findEventHandlers("calendar", "OnAfterCalendarEntryAdd") as $event)
				{
					ExecuteModuleEventEx(
						$event,
						[
							$eventId,
							$entryFields,
							[],
						]
					);
				}

				if (($entryFields['PARENT_ID'] ?? null) === $eventId && $entryFields['CAL_TYPE'] !== 'location')
				{
					Bitrix24Manager::increaseEventsAmount();
				}
			}
			else
			{
				foreach(EventManager::getInstance()->findEventHandlers("calendar", "OnAfterCalendarEntryUpdate") as $event)
				{
					ExecuteModuleEventEx(
						$event,
						[
							$eventId,
							$entryFields,
							$currentEvent['ATTENDEE_LIST'] ?? null,
						]
					);
				}
			}

			$pullUserId = (isset($entryFields['OWNER_ID']) && (int)$entryFields['OWNER_ID'] > 0)
                ? (int)$entryFields['OWNER_ID']
				: $userId
			;

			if (
				$pullUserId > 0
				&& ($params['overSaving'] ?? null) !== true
				&& self::$sendPush
			)
			{
				$entryFields['ID'] = $result;
				$parentEventId = $entryFields['PARENT_ID'] ?? $result;

				$entryFields = self::calculateUserOffset($pullUserId, $entryFields);

				if (isset($params['attendeeStatuses']))
				{
					$attendeeList = $params['attendeeStatuses'];
				}
				else
				{
					// TODO: open event handle later
					$attendeeListResult = $dbFields['CAL_TYPE'] === Dictionary::CALENDAR_TYPE['open_event']
						? self::getAttendeeList([], [$parentEventId])
						: self::getAttendeeList([$parentEventId])
					;
					$attendeeList = $attendeeListResult['attendeeList'][$parentEventId] ?? [];
				}

				$entryFields  = self::PreHandleEvent($entryFields);
				$skipTime = $entryFields['DT_SKIP_TIME'] === 'Y';
				$dateFromFormatted = self::getDateInJsFormat(
					CCalendar::createDateTimeObjectFromString($entryFields['DATE_FROM']),
					$skipTime,
				);
				$dateToFormatted = self::getDateInJsFormat(
					CCalendar::createDateTimeObjectFromString($entryFields['DATE_TO']),
					$skipTime,
				);
				$isDaylightSavingTimezone = !empty($entryFields['TZ_FROM'])
					? CCalendar::isDaylightSavingTimezone($entryFields['TZ_FROM'])
					: ''
				;

				$collabId = 0;

				if (
					!empty($entryFields['EVENT_TYPE'])
					&& in_array(
						$entryFields['EVENT_TYPE'],
						[Dictionary::EVENT_TYPE['collab'], Dictionary::EVENT_TYPE['shared_collab']],
						true
					)
				)
				{
					$collabId = self::getCollabIdByParentId($entryFields['PARENT_ID']);
				}

				Util::addPullEvent(
					PushCommand::EditEvent,
					$pullUserId,
					[
						'fields' => array_merge($entryFields, [
							'ATTENDEE_LIST' => $attendeeList,
							'DATE_FROM_FORMATTED' => $dateFromFormatted,
							'DATE_TO_FORMATTED' => $dateToFormatted,
							'IS_DAYLIGHT_SAVING_TZ' => $isDaylightSavingTimezone,
							'COLLAB_ID' => $collabId,
							'permissions' => self::getEventPermissions($entryFields, $pullUserId),
						]),
						'newEvent' => $isNewEvent,
						'requestUid' => $params['requestUid'] ?? null,
					]
				);
			}
		}

		return $result;
	}

	/**
	 * @param $id
	 * @param bool $checkPermissions
	 * @param bool $loadOriginalRecursion
	 * @return array|false
	 */
	public static function GetById($id, bool $checkPermissions = true, $loadOriginalRecursion = false)
	{
		if ($id > 0)
		{
			$event = self::GetList([
				'arFilter' => [
					'ID' => $id,
					'DELETED' => 'N',
				],
				'parseRecursion' => false,
				'fetchAttendees' => $checkPermissions,
				'checkPermissions' => $checkPermissions,
				'setDefaultLimit' => false,
				'loadOriginalRecursion' => $loadOriginalRecursion,
			]);
			if (!empty($event[0]) && is_array($event[0]))
			{
				return $event[0];
			}
		}

		return false;
	}

	public static function GetList($params = [])
	{
		$originalParams = $params;

		$isIntranetEnabled = CCalendar::IsIntranetEnabled();
		$checkPermissions = ($params['checkPermissions'] ?? null) !== false;
		$bCache = CCalendar::CacheTime() > 0;
		$params['setDefaultLimit'] = ($params['setDefaultLimit'] ?? null) === true;
		$userId = (isset($params['userId']) && $params['userId']) ? (int)$params['userId'] : CCalendar::GetCurUserId();
		$params['parseDescription'] = $params['parseDescription'] ?? true;
		$params['fetchAttendees'] = ($params['fetchAttendees'] ?? null) !== false;
		$resultEntryList = null;
		$userIndex = null;

		CTimeZone::Disable();
		if ($bCache)
		{
			$cache = new CPHPCache;
			$cacheId = 'eventlist'.md5(serialize($params)).CCalendar::GetOffset();
			if ($checkPermissions)
			{
				$cacheId .= 'perm' . CCalendar::GetCurUserId() . '|';
			}
			if (CCalendar::IsSocNet() && CCalendar::IsSocnetAdmin())
			{
				$cacheId .= 'socnetAdmin|';
			}
			$cachePath = CCalendar::CachePath().'event_list';

			if ($cache->InitCache(CCalendar::CacheTime(), $cacheId, $cachePath))
			{
				$cachedData = $cache->GetVars();
				if (isset($cachedData['dateTimeFormat']) && $cachedData['dateTimeFormat'] === FORMAT_DATETIME)
				{
					$resultEntryList = $cachedData["resultEntryList"];
					$userIndex = $cachedData["userIndex"];
				}
			}
		}

		if (!$bCache || !isset($resultEntryList))
		{
			$arFilter = $params['arFilter'];
			$resultEntryList = [];

			$listResult = self::getListOrm($params);
			[$eventList, $parentMeetingIdList, $involvedUsersIdList, $openEventParentIdList] = $listResult;

			if (!empty($params['fetchAttendees']) && !empty($parentMeetingIdList))
			{
				$attendeeListData = self::getAttendeeList($parentMeetingIdList, $openEventParentIdList);
				$attendeeList = $attendeeListData['attendeeList'];
				$involvedUsersIdList = array_unique(array_merge($involvedUsersIdList, $attendeeListData['userIdList']));
			}
			$userIndex = self::getUsersDetails($involvedUsersIdList);

			$parentCollabConnections = self::getParentCollabConnections($eventList);

			foreach ($eventList as $event)
			{
				$isOpenEvent = $event['CAL_TYPE'] === Dictionary::CALENDAR_TYPE['open_event'];
				if (
					$event['IS_MEETING']
					&& isset($attendeeList[$event['PARENT_ID']])
					&& $isIntranetEnabled
				)
				{
					if ($isOpenEvent)
					{
						// keep only STATUS=Y attendees for open events
						// cause there is no logic for decline user of open event
						$event['ATTENDEE_LIST'] = array_filter(
							$attendeeList[$event['PARENT_ID']],
							static fn (array $a) => $a['status'] === 'Y'
						);
					}
					else
					{
						$event['ATTENDEE_LIST'] = $attendeeList[$event['PARENT_ID']];
					}
				}
				else if (!empty($params['fetchAttendees']))
				{
					// for open event we not need to HOST user presented in attendee list
					if (!$isOpenEvent)
					{
						$event['ATTENDEE_LIST'] = [
							[
								'id' => (int)$event['MEETING_HOST'],
								'entryId' => $event['ID'],
								'status' => in_array($event['MEETING_STATUS'], ['Y', 'N', 'Q', 'H'])
									? $event['MEETING_STATUS']
									: 'H'
								,
							],
						];
					}
				}
				else
				{
					$event['ATTENDEE_LIST'] = [];
				}

				if ($checkPermissions)
				{
					$checkPermissionsForEvent = $userId !== (int)($event['CREATED_BY'] ?? null); // It's creator

					// It's event in user's calendar
					if (
						$checkPermissionsForEvent
						&& ($event['CAL_TYPE'] ?? null) === 'user'
						&& $userId === (int)$event['OWNER_ID']
					)
					{
						$checkPermissionsForEvent = false;
					}

					if (
						$checkPermissionsForEvent
						&& $event['IS_MEETING']
						&& is_array($event['ATTENDEE_LIST'] ?? null)
						&& !empty($event['ATTENDEE_LIST'])
					)
					{
						foreach($event['ATTENDEE_LIST'] as $attendee)
						{
							if ((int)$attendee['id'] === $userId)
							{
								$checkPermissionsForEvent = false;
								break;
							}
						}
					}

					if ($checkPermissionsForEvent)
					{
						$event = self::ApplyAccessRestrictions($event, $userId);
					}
				}

				if ($event !== false)
				{
					$event['COLLAB_ID'] = self::getCollabIdByEvent($event, $parentCollabConnections);

					$event = self::PreHandleEvent($event, [
						'parseDescription' => $params['parseDescription'],
					]);

					if (!empty($params['parseRecursion']) && self::CheckRecurcion($event))
					{
						self::ParseRecursion($resultEntryList, $event, [
							'userId' => $userId,
							'fromLimit' => $arFilter["FROM_LIMIT"] ?? null,
							'toLimit' => $arFilter["TO_LIMIT"] ?? null,
							'loadLimit' => $params["limit"] ?? null,
							'instanceCount' => $params['maxInstanceCount'] ?? false,
							'preciseLimits' => $params['preciseLimits'] ?? false,
						]);
					}
					else
					{
						self::HandleEvent($resultEntryList, $event, $userId);
					}
				}
			}

			if ($bCache)
			{
				$cache->StartDataCache(CCalendar::CacheTime(), $cacheId, $cachePath);

				try
				{
					$cache->EndDataCache(
						[
							'resultEntryList' => $resultEntryList,
							'userIndex' => $userIndex,
							'dateTimeFormat' => FORMAT_DATETIME,
						]
					);
				}
				catch (\Exception $e)
				{
					// A temporary fix for http://jabber.bx/view.php?id=215935
					$logger = new \Bitrix\Main\Diag\EventLogger('calendar', 'REMIND_DEBUG');

					$logger->debug('Found Closure. Message: ' . $e->getMessage());

					try
					{
						$logger->debug('Found Closure. Params: ' . \Bitrix\Main\Web\Json::encode($originalParams));
						$logger->debug(
							'Found Closure. To cache: ' . \Bitrix\Main\Web\Json::encode([
								'resultEntryList' => $resultEntryList,
								'userIndex' => $userIndex,
								'dateTimeFormat' => FORMAT_DATETIME,
							])
						);
					}
					catch (\Throwable)
					{}
				}
			}
		}

		if (is_array($userIndex))
		{
			foreach($userIndex as $userIndexId => $userIndexDetails)
			{
				self::$userIndex[$userIndexId] = $userIndexDetails;
			}
		}

		CTimeZone::Enable();

		return $resultEntryList;
	}

	private static function getListOrm($params = [])
	{
		$eventList = [];

		$userId = (isset($params['userId']) && $params['userId']) ? (int)$params['userId'] : CCalendar::GetCurUserId();
		$fetchSection = $params['fetchSection'] ?? null;
		$orderFields = $params['arOrder'] ?? [];
		$filterFields = $params['arFilter'] ?? [];
		$selectFields = $params['arSelect'] ?? [];
		$getUf = ($params['getUserfields'] ?? null) !== false;
		$eventFields = self::getEventFields();

		if (isset($filterFields["DELETED"]) && ($filterFields["DELETED"] === false))
		{
			unset($filterFields["DELETED"]);
		}
		elseif (!isset($filterFields["DELETED"]))
		{
			$filterFields["DELETED"] = "N";
		}

		if (($params['setDefaultLimit'] ?? null) !== false) // Deprecated
		{
			if (!isset($filterFields['FROM_LIMIT'])) // default 3 month back
			{
				$filterFields['FROM_LIMIT'] = CCalendar::Date(time() - 31 * 3 * 24 * 3600, false);
			}

			if (!isset($filterFields['TO_LIMIT'])) // default one year into the future
			{
				$filterFields['TO_LIMIT'] = CCalendar::Date(time() + 365 * 24 * 3600, false);
			}
		}

		$query = Internals\EventTable::query();
		$attendeesQuery = Internals\EventTable::query();
		$attendeeIds = [$userId];
		$joinOpenEvents = !empty(
			array_intersect(['ID', 'PARENT_ID', 'RECURRENCE_ID', 'CAL_TYPE', 'SECTION'], array_keys($filterFields))
		);

		if (!empty($filterFields) && is_array($filterFields))
		{
			foreach ($filterFields as $key => $value)
			{
				if (is_string($value) && !$value)
				{
					continue;
				}

				switch ($key)
				{
					case 'FROM_LIMIT':
						$timestamp = (int)CCalendar::Timestamp($value, false);
						if ($timestamp)
						{
							$query->where('DATE_TO_TS_UTC', '>=', $timestamp);
							$attendeesQuery->where('DATE_TO_TS_UTC', '>=', $timestamp);
						}
						break;
					case 'TO_LIMIT':
						$timestamp = (int)CCalendar::Timestamp($value, false);
						if ($timestamp)
						{
							$toTimestamp = $timestamp + CCalendar::GetDayLen() - 1;
							$query->where('DATE_FROM_TS_UTC', '<=', $toTimestamp);
							$attendeesQuery->where('DATE_FROM_TS_UTC', '<=', $toTimestamp);
						}
						break;
					case 'MEETING_HOST':
					case 'CREATED_BY':
						if (is_array($value))
						{
							$value = array_map(static function($item) {
								return (int)$item;
							}, $value);

							if (empty($value))
							{
								$value = [''];
							}

							$query->whereIn($key, $value);
						}
						else if ((int)$value)
						{
							$query->where($key, $value);
						}
						break;
					case 'ID':
					case 'PARENT_ID':
					case 'RECURRENCE_ID':
						if (!is_array($value))
						{
							$value = [$value];
						}

						$value = array_map('intval', $value);

						if (empty($value))
						{
							$value = [''];
						}

						$query->whereIn($key, $value);
						$attendeesQuery->whereIn($key, $value);

						break;
					case 'OWNER_ID':
						if (!is_array($value))
						{
							$value = [$value];
						}

						$value = array_map('intval', $value);

						if (empty($value))
						{
							$value = [''];
						}
						else
						{
							$attendeeIds = $value;
						}

						$query->whereIn('OWNER_ID', $value);

						break;
					case '>ID':
						if ((int)$value)
						{
							$query->where('ID', '>', $value);
						}
						break;
					case 'CAL_TYPE':
						if (!is_array($value))
						{
							$value = [$value];
						}

						$joinOpenEvents = $joinOpenEvents && in_array(Dictionary::CALENDAR_TYPE['open_event'], $value, true);

						$calTypes = array_diff($value, [Dictionary::CALENDAR_TYPE['open_event']]);
						if (!empty($calTypes))
						{
							$query->whereIn('CAL_TYPE', $calTypes);
						}

						break;
					case 'SECTION':
						if (!is_array($value))
						{
							$value = [$value];
						}

						if (is_array($value))
						{
							$sections = [];
							foreach ($value as $item)
							{
								if ((int)$item)
								{
									$sections[] = (int)$item;
								}
							}

							if (!empty($sections))
							{
								$openEventSection = self::getOpenEventSection($userId);
								$joinOpenEvents = $joinOpenEvents && in_array($openEventSection?->getId(), $sections, true);
								$sections = array_diff($sections, [$openEventSection?->getId()]);

								if (Util::isSectionStructureConverted())
								{
									$query->whereIn('SECTION_ID', $sections);
								}
								else
								{
									$query->whereIn('EVENT_SECT.SECT_ID', $sections);
								}
							}
						}
						break;
					case 'ACTIVE_SECTION':
						if ($value === 'Y' && Util::isSectionStructureConverted())
						{
							$query->where('SECTION.ACTIVE', $value);
						}
						break;
					case '*SEARCHABLE_CONTENT':
						$searchText = \Bitrix\Main\ORM\Query\Filter\Helper::matchAgainstWildcard($value);
						$query->whereMatch('SEARCHABLE_CONTENT', $searchText);
						break;
					case '*%SEARCHABLE_CONTENT':
						$query->whereLike('SEARCHABLE_CONTENT', '%' . $value . '%');
						break;
					case '=UF_CRM_CAL_EVENT':
						$query->where('UF_CRM_CAL_EVENT', $value);
						break;
					default:
						if (in_array($key, $eventFields, true))
						{
							if (is_array($value))
							{
								$query->whereIn($key, $value);
							}
							else
							{
								$query->where($key, $value);
							}
						}
						break;
				}
			}
		}

		if (empty($selectFields))
		{
			$selectFields = ['*'];
		}

		$attendeesQuery->setSelect([
			...$selectFields,
			'ATTENDEES.*',
			'EVENT_OPTIONS.*',
		]);

		$joinSection = $fetchSection
			&& ($filterFields['ACTIVE_SECTION'] ?? null) === 'Y'
			&& Util::isSectionStructureConverted()
		;

		if ($joinSection)
		{
			$selectFields['SECTION_DAV_XML_ID'] = 'SECTION.CAL_DAV_CAL';
		}

		if ($getUf)
		{
			$selectFields[] = 'UF_*';
		}

		$query
			->registerRuntimeField(
				new ReferenceField(
					'EVENT_OPTIONS',
					OpenEvents\Internals\OpenEventOptionTable::getEntity(),
					Join::on('this.ID', 'ref.EVENT_ID'),
				),
			);
		$selectFields[] = 'EVENT_OPTIONS.*';

		$query->setSelect($selectFields);

		$orderList = [];
		foreach ($orderFields as $key => $order)
		{
			if (in_array($key, $eventFields, true))
			{
				$orderList[$key] = (mb_strtoupper($order) === 'DESC') ? 'DESC' : 'ASC';
			}
		}

		if (!empty($orderList))
		{
			$query->setOrder($orderList);
			$attendeesQuery->setOrder($orderList);
		}

		if (isset($params['limit']) && (int)$params['limit'] > 0)
		{
			$query->setLimit((int)$params['limit']);
			$attendeesQuery->setLimit((int)$params['limit']);
		}

		if (($params['loadOriginalRecursion'] ?? null) === true)
		{
			self::applyLoadOriginalRecursionLogic($query);
		}

		$queryResult = $query->exec();
		$hasAttendees = $query->getEntity()->hasField('ATTENDEES');
		$hasOriginalRecursion = $query->getEntity()->hasField('ORIGINAL_RECURSION');

		$parentMeetingIdList = [];
		$involvedUsersIdList = [];

		$openEventList = [];

		while ($eventObject = $queryResult->fetchObject())
		{
			[$event, $parentMeetings, $involvedUsers] = self::prepareEventObject(
				$userId,
				$eventObject,
				$hasAttendees,
				$hasOriginalRecursion
			);

			$parentMeetingIdList = [...$parentMeetingIdList, ...$parentMeetings];
			$involvedUsersIdList = [...$involvedUsersIdList, ...$involvedUsers];

			$eventList[] = $event;

			// if open event presented in query results
			// add it in separate array to check in next query processing
			if (!empty($event['CAL_TYPE']) && $event['CAL_TYPE'] === Dictionary::CALENDAR_TYPE['open_event'])
			{
				$openEventList[] = (int)$event['ID'];
			}
		}

		if (!$joinOpenEvents)
		{
			return [$eventList, array_unique($parentMeetingIdList), array_unique($involvedUsersIdList), []];
		}

		$attendeesQuery
			->registerRuntimeField(
				(new ReferenceField(
					'ATTENDEES',
					Internals\EventAttendeeTable::getEntity(),
					Join::on('this.ID', 'ref.EVENT_ID')
						->whereIn('ref.OWNER_ID', $attendeeIds)
						->where('ref.MEETING_STATUS', 'Y')
						->where('ref.DELETED', 'N')
					,
				))
					->configureJoinType(Join::TYPE_INNER)
				,
			)
			->registerRuntimeField(
				new ReferenceField(
					'EVENT_OPTIONS',
					OpenEvents\Internals\OpenEventOptionTable::getEntity(),
					Join::on('this.ID', 'ref.EVENT_ID'),
				),
			)
			->where('DELETED', 'N')
			->where('CAL_TYPE', Dictionary::CALENDAR_TYPE['open_event'])
		;

		$attendeesQueryResult = $attendeesQuery->exec();
		$hasAttendees = $attendeesQuery->getEntity()->hasField('ATTENDEES');
		$openEventParentMeetingIdList = [];

		while ($eventObject = $attendeesQueryResult->fetchObject())
		{
			[$event, $parentMeetings, $involvedUsers] = self::prepareEventObject($userId, $eventObject, $hasAttendees);

			$parentMeetingIdList = [...$parentMeetingIdList, ...$parentMeetings];
			$involvedUsersIdList = [...$involvedUsersIdList, ...$involvedUsers];
			$openEventParentMeetingIdList = [...$openEventParentMeetingIdList, ...$parentMeetings];

			// replace same open_event from previous query with attendee field from current query to prevent duplicates
			// or add as new
			if (($key = array_search((int)$event['ID'], $openEventList, true)) !== false)
			{
				$eventList[$key] = $event;
			}
			else
			{
				$eventList[] = $event;
			}
		}

		return [
			$eventList,
			array_unique($parentMeetingIdList),
			array_unique($involvedUsersIdList),
			array_unique($openEventParentMeetingIdList),
		];
	}

	private static function prepareEventObject(
		int $userId,
		Internals\EO_Event $eventObject,
		bool $hasAttendees,
		bool $hasOriginalRecursion = false
	): array
	{
		$event = $eventObject->collectValues();

		//we don't need it here
		unset($event['UTS_OBJECT']);
		unset($event['SECTION']);

		// transform types from fetchObject-style to fetch-style
		// next booleans and strings fields sensitive for frontend
		// and also we need to support BC for rest api
		$stringFields = [
			'ID',
			'OWNER_ID',
			'CREATED_BY',
			'SECTION_ID',
			'TZ_OFFSET_FROM',
			'TZ_OFFSET_TO',
			'DATE_FROM_TS_UTC',
			'DATE_TO_TS_UTC',
			'MEETING_HOST',
		];
		foreach ($stringFields as $sField)
		{
			if (!isset($event[$sField]))
			{
				continue;
			}
			$event[$sField] = (string)$event[$sField];
		}

		$boolFields = [
			'ACTIVE',
			'DELETED',
			'DT_SKIP_TIME',
		];
		foreach ($boolFields as $bField)
		{
			if (!isset($event[$bField]))
			{
				continue;
			}
			$event[$bField] = $event[$bField] === true ? 'Y' : 'N';
		}

		// to prevent rest api method fails when try to on-fly create depth fields of MEETING array (like 0194353)
		$notEmptyFields = [
			'SYNC_STATUS' => '',
			'MEETING' => '',
			'EVENT_TYPE' => '',
			'ATTENDEES_CODES' => '',
			'RECURRENCE_ID' => 0,
		];
		foreach ($notEmptyFields as $nesField => $emptyValue)
		{
			if (!isset($event[$nesField]))
			{
				continue;
			}
			$event[$nesField] = $event[$nesField] === $emptyValue ? null : $event[$nesField];
		}

		$event['SECTION_DAV_XML_ID'] = $eventObject->getSection()?->getCalDavCal();

		if (isset($event['PARENT_ID']))
		{
			$event['PARENT_ID'] = (string)$event['PARENT_ID'];
		}

		$isFullDay = ($event['DT_SKIP_TIME'] ?? null) === 'Y';

		if (!empty($event['DATE_FROM']))
		{
			$event['DATE_FROM_FORMATTED'] = self::getDateInJsFormat($event['DATE_FROM'], $isFullDay);
			$event['DATE_FROM'] = (string)$event['DATE_FROM'];
		}

		if (!empty($event['DATE_TO']))
		{
			$event['DATE_TO_FORMATTED'] = self::getDateInJsFormat($event['DATE_TO'], $isFullDay);
			$event['DATE_TO'] = (string)$event['DATE_TO'];
		}

		if (!empty($event['ORIGINAL_DATE_FROM']))
		{
			$event['ORIGINAL_DATE_FROM'] = (string)$event['ORIGINAL_DATE_FROM'];
		}

		if (!empty($event['TIMESTAMP_X']))
		{
			$event['TIMESTAMP_X'] = (string)$event['TIMESTAMP_X'];
		}

		if (!empty($event['DATE_CREATE']))
		{
			$event['DATE_CREATE'] = (string)$event['DATE_CREATE'];
		}

		$event['IS_DAYLIGHT_SAVING_TZ'] = !empty($event['TZ_FROM'])
			? CCalendar::isDaylightSavingTimezone($event['TZ_FROM'])
			: ''
		;

		$event['SECT_ID'] = $event['SECTION_ID'] ?? null;
		if (
			(int)($event['IS_MEETING'] ?? 0) > 0
			|| ($event['CAL_TYPE'] ?? null) === Dictionary::CALENDAR_TYPE['open_event']
		)
		{
			$event['IS_MEETING'] = true;
		}
		else
		{
			$event['IS_MEETING'] = false;
		}

		if (empty($event['NAME']))
		{
			$event['NAME'] = Loc::getMessage('EC_T_NEW_EVENT');
		}
		else
		{
			$event['NAME'] = Emoji::decode($event['NAME']);
		}

		if (!empty($event['DESCRIPTION']))
		{
			$event['DESCRIPTION'] = Emoji::decode($event['DESCRIPTION']);
		}

		if (!empty($event['LOCATION']))
		{
			$event['LOCATION'] = Emoji::decode($event['LOCATION']);
		}

		if (!empty($event['DT_LENGTH']) && is_numeric($event['DT_LENGTH']))
		{
			$event['DT_LENGTH'] = (int)$event['DT_LENGTH'];
		}

		$parentMeetingIdList = [];
		$involvedUsersIdList = [];

		if (!empty($event['IS_MEETING']) && !empty($event['PARENT_ID']) && CCalendar::IsIntranetEnabled())
		{
			$parentMeetingIdList[] = $event['PARENT_ID'];
		}

		if (!empty($event['CREATED_BY']))
		{
			$involvedUsersIdList[] = $event['CREATED_BY'];
		}

		if (
			!empty($event['IS_MEETING'])
			&& $event['CAL_TYPE'] === Dictionary::CALENDAR_TYPE['user']
			&& (int)$event['OWNER_ID'] === $userId
			&& !$event['SECTION_ID']
		)
		{
			$defaultMeetingSection = CCalendar::GetMeetingSection($userId);
			if (!$defaultMeetingSection || !CCalendarSect::GetById($defaultMeetingSection, false))
			{
				$sectRes = CCalendarSect::GetSectionForOwner($event['CAL_TYPE'], $userId);
				$defaultMeetingSection = $sectRes['sectionId'];
			}

			// to support rest api BC cast to string
			$event['SECT_ID'] = (string)$defaultMeetingSection;
			$event['SECTION_ID'] = (string)$defaultMeetingSection;
		}

		$eventOptions = $eventObject->get('EVENT_OPTIONS');
		$event['OPTIONS'] = $eventOptions ? $eventOptions->collectValues() : null;

		// replace fields at host event from external attendee
		/** @var Internals\EO_EventAttendee $currentAttendee */
		if ($hasAttendees && ($currentAttendee = $eventObject->get('ATTENDEES')))
		{
			//TODO: for test purposes keep it for a while
			// this rows should stored only on actual event table
//			if (isset($event['OWNER_ID']))
//			{
//				$event['OWNER_ID'] = $currentAttendee->getOwnerId();
//			}
//			if (isset($event['CREATED_BY']))
//			{
//				$event['CREATED_BY'] = $currentAttendee->getCreatedBy();
//			}
//			$event['~TYPE'] = 'open_event';
			if (isset($event['MEETING_STATUS']))
			{
				$event['MEETING_STATUS'] = $currentAttendee->getMeetingStatus();
			}
			if (isset($event['SECTION_ID']))
			{
				// to support rest api BC cast to string
				$event['SECTION_ID'] = (string)$currentAttendee->getSectionId();
				$event['SECT_ID'] = (string)$currentAttendee->getSectionId();
			}
			if (!empty($currentAttendee->getColor()))
			{
				$event['COLOR'] = $currentAttendee->getColor();
			}
			if (isset($event['REMIND']))
			{
				$event['REMIND'] = $currentAttendee->getRemind();
			}
			if (isset($event['SYNC_STATUS']))
			{
				$event['SYNC_STATUS'] = $currentAttendee->getSyncStatus();
			}
			$event['CURRENT_ATTENDEE_ID'] = $currentAttendee->getId();
			$parentMeetingIdList[] = $event['ID'];
		}

		/** @var Internals\EO_EventOriginalRecursion $originalRecursion */
		if (
			$hasOriginalRecursion
			&& $originalRecursion = $eventObject->get('ORIGINAL_RECURSION')
		)
		{
			$event['ORIGINAL_RECURSION_ID'] = $originalRecursion->getOriginalRecursionEventId();
		}

		return [$event, $parentMeetingIdList, $involvedUsersIdList];
	}

	private static function applyLoadOriginalRecursionLogic(Query $query): void
	{
		$query->registerRuntimeField(
			(new Reference(
				'ORIGINAL_RECURSION',
				Internals\EventOriginalRecursionTable::class,
				Join::on('this.PARENT_ID', 'ref.PARENT_EVENT_ID'),
			))
				->configureJoinType(Join::TYPE_LEFT)
		);
		$query->setSelect(
			array_merge(
				$query->getSelect(),
				['ORIGINAL_RECURSION_ID' => 'ORIGINAL_RECURSION.ORIGINAL_RECURSION_EVENT_ID']
			)
		);
	}


	private static function getDateInJsFormat(DateTime|Bitrix\Main\Type\DateTime $date, $isFullDay): string
	{
		if ($isFullDay)
		{
			return $date->format('D M d Y');
		}

		return $date->format('D M d Y H:i:s');
	}

	private static function GetFields()
	{
		global $DB;
		if (!count(self::$fields))
		{
			CTimeZone::Disable();
			self::$fields = array(
				"ID" => Array("FIELD_NAME" => "CE.ID", "FIELD_TYPE" => "int"),
				"PARENT_ID" => Array("FIELD_NAME" => "CE.PARENT_ID", "FIELD_TYPE" => "int"),
				"DELETED" => Array("FIELD_NAME" => "CE.DELETED", "FIELD_TYPE" => "string"),
				"CAL_TYPE" => Array("FIELD_NAME" => "CE.CAL_TYPE", "FIELD_TYPE" => "string"),
				"SYNC_STATUS" => Array("FIELD_NAME" => "CE.SYNC_STATUS", "FIELD_TYPE" => "string"),
				"OWNER_ID" => Array("FIELD_NAME" => "CE.OWNER_ID", "FIELD_TYPE" => "int"),
				"EVENT_TYPE" => Array("FIELD_NAME" => "CE.EVENT_TYPE", "FIELD_TYPE" => "string"),
				"CREATED_BY" => Array("FIELD_NAME" => "CE.CREATED_BY", "FIELD_TYPE" => "int"),
				"NAME" => Array("FIELD_NAME" => "CE.NAME", "FIELD_TYPE" => "string"),
				"DATE_FROM" => Array("FIELD_NAME" => $DB->DateToCharFunction("CE.DATE_FROM").' as DATE_FROM', "FIELD_TYPE" => "date"),
				"DATE_TO" => Array("FIELD_NAME" => $DB->DateToCharFunction("CE.DATE_TO").' as DATE_TO', "FIELD_TYPE" => "date"),
				"TZ_FROM" => Array("FIELD_NAME" => "CE.TZ_FROM", "FIELD_TYPE" => "string"),
				"TZ_TO" => Array("FIELD_NAME" => "CE.TZ_TO", "FIELD_TYPE" => "string"),
				"ORIGINAL_DATE_FROM" => Array("FIELD_NAME" => $DB->DateToCharFunction("CE.ORIGINAL_DATE_FROM").' as ORIGINAL_DATE_FROM', "FIELD_TYPE" => "date"),
				"TZ_OFFSET_FROM" => Array("FIELD_NAME" => "CE.TZ_OFFSET_FROM", "FIELD_TYPE" => "int"),
				"TZ_OFFSET_TO" => Array("FIELD_NAME" => "CE.TZ_OFFSET_TO", "FIELD_TYPE" => "int"),
				"DATE_FROM_TS_UTC" => Array("FIELD_NAME" => "CE.DATE_FROM_TS_UTC", "FIELD_TYPE" => "int"),
				"DATE_TO_TS_UTC" => Array("FIELD_NAME" => "CE.DATE_TO_TS_UTC", "FIELD_TYPE" => "int"),

				"TIMESTAMP_X" => Array("FIELD_NAME" => $DB->DateToCharFunction("CE.TIMESTAMP_X").' as TIMESTAMP_X', "FIELD_TYPE" => "date"),
				"DATE_CREATE" => Array("FIELD_NAME" => $DB->DateToCharFunction("CE.DATE_CREATE").' as DATE_CREATE', "FIELD_TYPE" => "date"),
				"DESCRIPTION" => Array("FIELD_NAME" => "CE.DESCRIPTION", "FIELD_TYPE" => "string"),
				"DT_SKIP_TIME" => Array("FIELD_NAME" => "CE.DT_SKIP_TIME", "FIELD_TYPE" => "string"),
				"DT_LENGTH" => Array("FIELD_NAME" => "CE.DT_LENGTH", "FIELD_TYPE" => "int"),
				"PRIVATE_EVENT" => Array("FIELD_NAME" => "CE.PRIVATE_EVENT", "FIELD_TYPE" => "string"),
				"ACCESSIBILITY" => Array("FIELD_NAME" => "CE.ACCESSIBILITY", "FIELD_TYPE" => "string"),
				"IMPORTANCE" => Array("FIELD_NAME" => "CE.IMPORTANCE", "FIELD_TYPE" => "string"),
				"IS_MEETING" => Array("FIELD_NAME" => "CE.IS_MEETING", "FIELD_TYPE" => "string"),
				"MEETING_HOST" => Array("FIELD_NAME" => "CE.MEETING_HOST", "FIELD_TYPE" => "int"),
				"MEETING_STATUS" => Array("FIELD_NAME" => "CE.MEETING_STATUS", "FIELD_TYPE" => "string"),
				"MEETING" => Array("FIELD_NAME" => "CE.MEETING", "FIELD_TYPE" => "string"),
				"LOCATION" => Array("FIELD_NAME" => "CE.LOCATION", "FIELD_TYPE" => "string"),
				"REMIND" => Array("FIELD_NAME" => "CE.REMIND", "FIELD_TYPE" => "string"),
				"COLOR" => Array("FIELD_NAME" => "CE.COLOR", "FIELD_TYPE" => "string"),
				"RRULE" => Array("FIELD_NAME" => "CE.RRULE", "FIELD_TYPE" => "string"),
				"EXDATE" => Array("FIELD_NAME" => "CE.EXDATE", "FIELD_TYPE" => "string"),
				"ATTENDEES_CODES" => Array("FIELD_NAME" => "CE.ATTENDEES_CODES", "FIELD_TYPE" => "string"),
				"DAV_XML_ID" => Array("FIELD_NAME" => "CE.DAV_XML_ID", "FIELD_TYPE" => "string"), //
				"DAV_EXCH_LABEL" => Array("FIELD_NAME" => "CE.DAV_EXCH_LABEL", "FIELD_TYPE" => "string"), // Exchange sync label
				"G_EVENT_ID" => Array("FIELD_NAME" => "CE.G_EVENT_ID", "FIELD_TYPE" => "string"), // Google event id
				"CAL_DAV_LABEL" => Array("FIELD_NAME" => "CE.CAL_DAV_LABEL", "FIELD_TYPE" => "string"), // CalDAV sync label
				"VERSION" => Array("FIELD_NAME" => "CE.VERSION", "FIELD_TYPE" => "string"), // Version used for outlook sync
				"RECURRENCE_ID" => Array("FIELD_NAME" => "CE.RECURRENCE_ID", "FIELD_TYPE" => "int"),
				"RELATIONS" => Array("FIELD_NAME" => "CE.RELATIONS", "FIELD_TYPE" => "int"),
				"SEARCHABLE_CONTENT" => Array("FIELD_NAME" => "CE.SEARCHABLE_CONTENT", "FIELD_TYPE" => "string"),
				"SECTION_ID" => Array("FIELD_NAME" => "CE.SECTION_ID", "FIELD_TYPE" => "int"),
			);
			CTimeZone::Enable();
		}
		return self::$fields;
	}

	private static function getEventFields(): array
	{
		return [
			'ID',
			'PARENT_ID',
			'DELETED',
			'CAL_TYPE',
			'SYNC_STATUS',
			'OWNER_ID',
			'EVENT_TYPE',
			'CREATED_BY',
			'NAME',
			'DATE_FROM',
			'DATE_TO',
			'TZ_FROM',
			'TZ_TO',
			'ORIGINAL_DATE_FROM',
			'TZ_OFFSET_FROM',
			'TZ_OFFSET_TO',
			'DATE_FROM_TS_UTC',
			'DATE_TO_TS_UTC',
			'TIMESTAMP_X',
			'DATE_CREATE',
			'DESCRIPTION',
			'DT_SKIP_TIME',
			'DT_LENGTH',
			'PRIVATE_EVENT',
			'ACCESSIBILITY',
			'IMPORTANCE',
			'IS_MEETING',
			'MEETING_HOST',
			'MEETING_STATUS',
			'MEETING',
			'LOCATION',
			'REMIND',
			'COLOR',
			'RRULE',
			'EXDATE',
			'ATTENDEES_CODES',
			'DAV_XML_ID',
			'DAV_EXCH_LABEL',
			'G_EVENT_ID',
			'CAL_DAV_LABEL',
			'VERSION',
			'RECURRENCE_ID',
			'RELATIONS',
			'SECTION_ID',
		];
	}

	/**
	 * @deprecated
	 */
	public static function ConnectEventToSection($eventId, $sectionId)
	{
		global $DB;
		$DB->Query(
			"DELETE FROM b_calendar_event_sect WHERE EVENT_ID=". (int)$eventId);

		$DB->Query(
			"INSERT INTO b_calendar_event_sect(EVENT_ID, SECT_ID) ".
			"SELECT ". (int)$eventId .", ID ".
			"FROM b_calendar_section ".
			"WHERE ID=". (int)$sectionId);
	}

	/**
	 * selects all participants of the event by parentId
	 */
	public static function getAttendeeList($entryIdList = [], array $openEventIdList = []): array
	{
		$attendeeList = [];
		$userIdList = [];

		if (!CCalendar::IsSocNet())
		{
			return [
				'attendeeList' => $attendeeList,
				'userIdList' => $userIdList,
			];
		}

		$entryIdList = is_array($entryIdList)
			? array_map('intval', array_unique($entryIdList))
			: [(int)$entryIdList]
		;

		if (empty($entryIdList) && empty($openEventIdList))
		{
			return [
				'attendeeList' => $attendeeList,
				'userIdList' => $userIdList,
			];
		}

		$queries = [];
		if (!empty($entryIdList))
		{
			$queries[] = Internals\EventTable::query()
				->setSelect([
					'USER_ID' => 'OWNER_ID',
					'ID',
					'PARENT_ID',
					'EVENT_MEETING_STATUS' => 'MEETING_STATUS',
					'MEETING_HOST',
				])
				->where('ACTIVE', 'Y')
				->where('CAL_TYPE', 'user')
				->where('DELETED', 'N')
				->whereIn('PARENT_ID', $entryIdList)
				->exec()
			;
		}

		if (!empty($openEventIdList))
		{
			// for event which stored with external attendees in b_calendar_event_attendee
			$queries[] = Internals\EventTable::query()
				->registerRuntimeField(
					(new ReferenceField(
						'EVENT_ATTENDEE',
						Internals\EventAttendeeTable::getEntity(),
						Join::on('this.ID', 'ref.EVENT_ID')
					))->configureJoinType(Join::TYPE_INNER),
				)
				->setSelect([
					'USER_ID' => 'EVENT_ATTENDEE.OWNER_ID',
					'ID',
					'PARENT_ID',
					'EVENT_MEETING_STATUS' => 'EVENT_ATTENDEE.MEETING_STATUS',
					'MEETING_HOST',
					'ATTENDEE_ID' => 'EVENT_ATTENDEE.ID',
				])
				->where('ACTIVE', 'Y')
				->where('DELETED', 'N')
				->whereIn('PARENT_ID', $openEventIdList)
				->where(
					(new ConditionTree())
						->logic(ConditionTree::LOGIC_OR)
						->where('EVENT_ATTENDEE.MEETING_STATUS', 'Y')
						->whereNot('CAL_TYPE', Dictionary::CALENDAR_TYPE['open_event'])
				)
				->exec()
			;
		}

		// generator for union-like joining queries
		$queryCombineGenerator = static function (array $queryResults): \Generator
		{
			foreach ($queryResults as $queryResult)
			{
				while($item = $queryResult->fetch())
				{
					yield $item;
				}
			}
		};

		// combine query result in one virtual set
		$combinedQuery = $queryCombineGenerator($queries);

		while($entry = $combinedQuery->current())
		{
			$combinedQuery->next();
			$entry['USER_ID'] = (int)$entry['USER_ID'];
			if (!isset($attendeeList[$entry['PARENT_ID']]))
			{
				$attendeeList[$entry['PARENT_ID']] = [];
			}
			$entry["STATUS"] = trim($entry['EVENT_MEETING_STATUS']);
			if (
				($entry['PARENT_ID'] === $entry['ID'] || $entry['USER_ID'] === $entry['MEETING_HOST'])
				&& !isset($entry['ATTENDEE_ID'])
			)
			{
				$entry["STATUS"] = "H";
			}
			if (!isset($attendeeList[$entry['PARENT_ID']][$entry['USER_ID']])) {
				$attendeeList[$entry['PARENT_ID']][$entry['USER_ID']] = [
					'id' => $entry['USER_ID'],
					'entryId' => $entry['ID'],
					'status' => $entry["STATUS"],
				];
			}

			if (!in_array($entry['USER_ID'], $userIdList, true))
			{
				$userIdList[] = $entry['USER_ID'];
			}
		}
		// remove doubles attendees from events
		// which stored in both b_calendar_event & b_calendar_event_attendee tables
		$attendeeList = array_map(static fn($itemList) => array_values($itemList), $attendeeList);

		return [
			'attendeeList' => $attendeeList,
			'userIdList' => $userIdList,
		];
	}

	public static function getUsersDetails($userIdList = [], $params = [])
	{
		$users = [];
		$userList = [];
		if (!empty($userIdList))
		{
			$userIdList = array_unique(array_map(static fn($userId) => (int)$userId, $userIdList));

			$userList = CCalendar::GetUserList($userIdList);
		}

		foreach ($userList as $userData)
		{
			$id = (int)$userData['ID'];
			if (!in_array($id, $userIdList, true))
			{
				continue;
			}

			$users[$userData['ID']] = [
				'ID' => $userData['ID'],
				'DISPLAY_NAME' => CCalendar::GetUserName($userData),
				'URL' => CCalendar::GetUserUrl($userData['ID']),
				'AVATAR' => CCalendar::GetUserAvatarSrc($userData, $params),
				'EMAIL_USER' => $userData['EXTERNAL_AUTH_ID'] === 'email',
				'SHARING_USER' => $userData['EXTERNAL_AUTH_ID'] === Sharing\SharingUser::EXTERNAL_AUTH_ID,
				'COLLAB_USER' => $userData['COLLAB_USER'],
			];
		}

		return $users;
	}

	public static function GetAttendees($eventIdList = [], $checkDeleted = true)
	{
		global $DB;
		$attendees = [];

		if (CCalendar::IsSocNet())
		{
			$eventIdList = is_array($eventIdList) ? array_map('intval', array_unique($eventIdList)) : [(int)$eventIdList];

			if (!empty($eventIdList))
			{
				$deletedCondition = $checkDeleted ? "CE.DELETED = 'N' AND" : '';
				$strSql = "
				SELECT
					CE.OWNER_ID AS USER_ID,
					CE.ID, CE.PARENT_ID, CE.MEETING_STATUS, CE.MEETING_HOST,
					U.LOGIN, U.NAME, U.LAST_NAME, U.SECOND_NAME, U.EMAIL, U.PERSONAL_PHOTO, U.WORK_POSITION, U.EXTERNAL_AUTH_ID,
					BUF.UF_DEPARTMENT
				FROM
					b_calendar_event CE
					LEFT JOIN b_user U ON (U.ID=CE.OWNER_ID)
					LEFT JOIN b_uts_user BUF ON (BUF.VALUE_ID = CE.OWNER_ID)
				WHERE
					CE.ACTIVE = 'Y' AND
					CE.CAL_TYPE = 'user' AND
					{$deletedCondition}
					CE.PARENT_ID in (".implode(',', $eventIdList).")";

				$res = $DB->Query($strSql);
				while($entry = $res->Fetch())
				{
					$parentId = (int)$entry['PARENT_ID'];
					$attendeeId = (int)$entry['USER_ID'];
					if (!isset($attendees[$parentId]))
					{
						$attendees[$parentId] = [];
					}
					$entry["STATUS"] = trim($entry["MEETING_STATUS"]);
					if ($parentId === (int)$entry['ID'])
					{
						$entry["STATUS"] = "H";
					}

					CCalendar::SetUserDepartment($attendeeId, (empty($entry['UF_DEPARTMENT'])
						? []
						: unserialize($entry['UF_DEPARTMENT'], ['allowed_classes' => false])));
					$entry['DISPLAY_NAME'] = CCalendar::GetUserName($entry);
					$entry['URL'] = CCalendar::GetUserUrl($attendeeId);
					$entry['AVATAR'] = CCalendar::GetUserAvatarSrc($entry);
					$entry['EVENT_ID'] = $entry['ID'];

					unset($entry['ID'], $entry['PARENT_ID'], $entry['UF_DEPARTMENT'], $entry['LOGIN']);
					$attendees[$parentId][] = $entry;
				}
			}
		}

		return $attendees;
	}

	public static function GetAttendeeIds(array $eventIdList): array
	{
		$eventIdList = array_map('intval', array_unique($eventIdList));

		if (empty($eventIdList))
		{
			return [];
		}

		$attendeeIds = [];
		$entries = Internals\EventTable::query()
			->setSelect(['PARENT_ID', 'CAL_TYPE', 'OWNER_ID', 'ACTIVE', 'DELETED'])
			->whereIn('PARENT_ID', $eventIdList)
			->where('CAL_TYPE', Dictionary::CALENDAR_TYPE['user'])
			->where('ACTIVE', 'Y')
			->where('DELETED', 'N')
			->exec()
			->fetchAll()
		;

		foreach ($entries as $entry)
		{
			$parentId = $entry['PARENT_ID'];
			$attendeeIds[$parentId] ??= [];
			$attendeeIds[$parentId][] = (int)$entry['OWNER_ID'];
		}

		return $attendeeIds;
	}

	public static function ApplyAccessRestrictions($event, $userId = null)
	{
		if (!$userId)
		{
			$userId = CCalendar::GetUserId();
		}

		$sectId = (int)$event['SECT_ID'];
		if (empty($event['ACCESSIBILITY']))
		{
			$event['ACCESSIBILITY'] = 'busy';
		}

		$private = !empty($event['PRIVATE_EVENT']) && ($event['CAL_TYPE'] ?? null) === 'user';
		$isAttendee = false;

		if (is_array($event['ATTENDEE_LIST'] ?? null))
		{
			foreach($event['ATTENDEE_LIST'] as $attendee)
			{
				if ((int)$attendee['id'] === (int)$userId)
				{
					$isAttendee = true;
					break;
				}
			}
		}

		if (
            ($event['CAL_TYPE'] ?? null) === 'user'
			&& !empty($event['IS_MEETING'])
			&& (int)$event['OWNER_ID'] !== (int)$userId
			&& $isAttendee
		)
		{
			$sectId = (int)CCalendar::GetMeetingSection($userId);
		}

		$accessResult = self::checkEventAccessFromGetList($event, $sectId, $userId);

		if ($private || (!$accessResult[ActionDictionary::ACTION_EVENT_VIEW_FULL] && !$isAttendee))
		{
			if ($private)
			{
				$event['NAME'] = '[' . Loc::getMessage('EC_ACCESSIBILITY_' . mb_strtoupper($event['ACCESSIBILITY'])) . ']';
				$event['IS_ACCESSIBLE_TO_USER'] = false;

				if (!$accessResult[ActionDictionary::ACTION_EVENT_VIEW_TIME])
				{
					return false;
				}
			}
			else if (!$accessResult[ActionDictionary::ACTION_EVENT_VIEW_TITLE])
			{
				if ($accessResult[ActionDictionary::ACTION_EVENT_VIEW_TIME])
				{
					$event['NAME'] = '[' . Loc::getMessage('EC_ACCESSIBILITY_' . mb_strtoupper($event['ACCESSIBILITY'])) . ']';
					$event['IS_ACCESSIBLE_TO_USER'] = false;
				}
				else
				{
					return false;
				}
			}
			else
			{
				$event['NAME'] .= ' [' . Loc::getMessage('EC_ACCESSIBILITY_' . mb_strtoupper($event['ACCESSIBILITY'])) . ']';
			}

			// Clear information about
			unset(
				$event['DESCRIPTION'],
				$event['LOCATION'],
				$event['REMIND'],
				$event['ATTENDEE_LIST'],
				$event['ATTENDEES_CODES'],
                $event['UF_CRM_CAL_EVENT'],
                $event['UF_WEBDAV_CAL_EVENT'],
			);
		}

		return $event;
	}

	public static function convertDateToCulture(string $str): string
	{
		if (CCalendar::DFormat(false) !== ExcludedDatesCollection::EXCLUDED_DATE_FORMAT)
		{
			if (preg_match_all("/(\d{2})\.(\d{2})\.(\d{4})/", $str, $matches))
			{
				foreach ($matches[0] as $index => $match)
				{
					$newValue = CCalendar::Date(
						mktime(
							0,
							0,
							0,
							$matches[2][$index],
							$matches[1][$index],
							$matches[3][$index]
						),
						false
					);
					$str = str_replace($match, $newValue, $str);
				}
			}
		}

		return $str;
	}

	private static function convertExDatesToInternalFormat(string $exDateString): string
	{
		if (!empty($exDateString))
		{
			$exDates = explode(';', $exDateString);
			$result = [];
			foreach ($exDates as $exDate)
			{
				$result[] = self::convertDateToRecurrenceFormat($exDate);
			}
			$exDateString = implode(';', $result);
		}

		return $exDateString;
	}
	private static function convertRuleUntilToInternalFormat(?string $untilString): ?string
	{
		if (!empty($untilString) && preg_match('/UNTIL=(.+)[;$]/U', $untilString, $matches))
		{
			$internalFormatedDate = self::convertDateToRecurrenceFormat($matches[1]);
			$untilString = str_replace($matches[1], $internalFormatedDate, $untilString);
		}

		return $untilString;
	}

	private static function convertDateToRecurrenceFormat(string $date = ''): string
	{
		if (CCalendar::DFormat(false) !== ExcludedDatesCollection::EXCLUDED_DATE_FORMAT)
		{
			$date = date(
				ExcludedDatesCollection::EXCLUDED_DATE_FORMAT,
				CCalendar::Timestamp($date)
			);
		}

		return $date;
	}

	private static function PreHandleEvent($item, $params = [])
	{
		if (!empty($item['LOCATION']))
		{
			$item['LOCATION'] = trim($item['LOCATION']);
		}

		if (!empty($item['MEETING']))
		{
			$item['MEETING'] = unserialize($item['MEETING'], ['allowed_classes' => false]);

			if (!is_array($item['MEETING']))
			{
				$item['MEETING'] = [];
			}
		}

		if (!empty($item['RELATIONS']))
		{
			$item['RELATIONS'] = unserialize($item['RELATIONS'], ['allowed_classes' => false]);

			if (!is_array($item['RELATIONS']))
			{
				$item['RELATIONS'] = [];
			}
		}

		if (!empty($item['REMIND']))
		{
			$item['REMIND'] = unserialize($item['REMIND'], ['allowed_classes' => false]);

			if (!is_array($item['REMIND']))
			{
				$item['REMIND'] = [];
			}
		}

		if (!empty($item['IS_MEETING']) && !empty($item['MEETING']) && !is_array($item['MEETING']))
		{
			$item['MEETING'] = unserialize($item['MEETING'], ['allowed_classes' => false]);

			if (!is_array($item['MEETING']))
			{
				$item['MEETING'] = [];
			}
		}

		if (self::CheckRecurcion($item))
		{
			$item['EXDATE'] = !empty($item['EXDATE']) ? self::convertDateToCulture($item['EXDATE']) : '';
			$item['RRULE'] = self::ParseRRULE(self::convertDateToCulture($item['RRULE']));
			$item['~RRULE_DESCRIPTION'] = self::GetRRULEDescription($item);
			$tsFrom = CCalendar::Timestamp($item['DATE_FROM']);
			$tsTo = CCalendar::Timestamp($item['DATE_TO']);
			if (($tsTo - $tsFrom) > $item['DT_LENGTH'] + CCalendar::DAY_LENGTH)
			{
				$toTS = $tsFrom + $item['DT_LENGTH'];
				if (isset($item['DT_SKIP_TIME']) && $item['DT_SKIP_TIME'] === 'Y')
				{
					$toTS -= CCalendar::GetDayLen();
				}
				$item['DATE_TO'] = CCalendar::Date($toTS);
			}
		}

		if (!empty($item['ATTENDEES_CODES']) && is_string($item['ATTENDEES_CODES']))
		{
			$item['ATTENDEES_CODES'] = explode(',', $item['ATTENDEES_CODES']);
			$item['attendeesEntityList'] = Util::convertCodesToEntities($item['ATTENDEES_CODES'] ?? null);
		}

		if (
			!empty($item['IS_MEETING'])
			&& (int)$item['ID'] === (int)$item['PARENT_ID']
			&& !($item['CURRENT_ATTENDEE_ID'] ?? null)
		)
		{
			$item['MEETING_STATUS'] = 'H';
		}

		$item['DT_SKIP_TIME'] = ($item['DT_SKIP_TIME'] ?? null) === 'Y' ? 'Y' : 'N';


		if (empty($item['IMPORTANCE']))
		{
			$item['IMPORTANCE'] = 'normal';
		}

		$item['PRIVATE_EVENT'] = trim((string)($item['PRIVATE_EVENT'] ?? null));

		$item['DESCRIPTION'] = trim((string)($item['DESCRIPTION'] ?? null));

		if (!empty($params['parseDescription']))
		{
			$item['~DESCRIPTION'] = self::ParseText(
				$item['DESCRIPTION'],
				!empty($item['PARENT_ID']) ? $item['PARENT_ID'] : $item['ID'],
				$item['UF_WEBDAV_CAL_EVENT'] ?? null
			);
		}

		if (isset($item['UF_CRM_CAL_EVENT']) && is_array($item['UF_CRM_CAL_EVENT']) && empty($item['UF_CRM_CAL_EVENT']))
		{
			$item['UF_CRM_CAL_EVENT'] = '';
		}

		unset($item['SEARCHABLE_CONTENT']);

		return $item;
	}

	public static function CheckRecurcion($event)
	{
		return !empty($event['RRULE']);
	}

	public static function ParseText($text = "", $eventId = 0, $arUFWDValue = [])
	{
		if ($text)
		{
			if (!is_object(self::$TextParser))
			{
				self::$TextParser = new CTextParser();
				self::$TextParser->allow = array(
					"HTML" => "N",
					"ANCHOR" => "Y",
					"BIU" => "Y",
					"IMG" => "Y",
					"QUOTE" => "Y",
					"CODE" => "Y",
					"FONT" => "Y",
					"LIST" => "Y",
					"SMILES" => "Y",
					"NL2BR" => "Y",
					"VIDEO" => "Y",
					"TABLE" => "Y",
					"CUT_ANCHOR" => "N",
					"ALIGN" => "Y",
					"USER" => "Y",
				);
			}

			self::$TextParser->allow["USERFIELDS"] = self::getUFForParseText($eventId, $arUFWDValue);
			$text = self::$TextParser->convertText($text);
			$text = preg_replace("/<br \/>/i", "<br>", $text);
		}
		return $text;
	}

	public static function getUFForParseText($eventId = 0, $arUFWDValue = []): array
	{
		$userFields = self::GetEventUserFields(['ID' => $eventId]);
		$userFields = [
			'UF_WEBDAV_CAL_EVENT' => $userFields['UF_WEBDAV_CAL_EVENT'] ?? [],
		];

		if (empty($arUFWDValue))
		{
			$arUFWDValue = $userFields['UF_WEBDAV_CAL_EVENT']['VALUE'] ?? [];
		}

		$userFields['UF_WEBDAV_CAL_EVENT']['VALUE'] = $arUFWDValue;
		$userFields['UF_WEBDAV_CAL_EVENT']['ENTITY_VALUE_ID'] = $eventId;

		return $userFields;
	}

	public static function ParseRecursion(&$res, $event, $params = [])
	{
		$event['DT_LENGTH'] = (int)$event['DT_LENGTH'];// length in seconds
		$event['RRULE'] = self::ParseRRULE($event['RRULE']);
		$length = $event['DT_LENGTH'];
		$skipTime = $event['DT_SKIP_TIME'] === 'Y';

		$rrule = $event['RRULE'];
		$exDate = self::GetExDate($event['EXDATE'] ?? null);
		$tsFrom = CCalendar::Timestamp($event['DATE_FROM']);
		$tsTo = CCalendar::Timestamp($event['DATE_TO']);

		if (($tsTo - $tsFrom) > $event['DT_LENGTH'] + CCalendar::DAY_LENGTH)
		{
			$toTS = $tsFrom + $length;
			if ($skipTime)
			{
				$toTS -= CCalendar::GetDayLen();
			}

			$event['DATE_TO'] = CCalendar::Date($toTS);
		}

		$h24 = CCalendar::GetDayLen();
		$instanceCount = ($params['instanceCount'] && $params['instanceCount'] > 0) ? $params['instanceCount'] : false;
		$loadLimit = ($params['loadLimit'] && $params['loadLimit'] > 0) ? $params['loadLimit'] : false;

		$preciseLimits = $params['preciseLimits'];

		if ($length < 0) // Protection from infinite recursion
		{
			$length = $h24;
		}

		// Time boundaries
		if (isset($params['fromLimitTs']))
		{
			$limitFromTS = (int)$params['fromLimitTs'];
		}
		else if (!empty($params['fromLimit']))
		{
			$limitFromTS = CCalendar::Timestamp($params['fromLimit']);
		}
		else
		{
			$limitFromTS = CCalendar::Timestamp(CCalendar::GetMinDate());
		}

		if (isset($params['toLimitTs']))
		{
			$limitToTS = (int)$params['toLimitTs'];
		}
		else if (!empty($params['toLimit']))
		{
			$limitToTS = CCalendar::Timestamp($params['toLimit']);
		}
		else
		{
			$limitToTS = CCalendar::Timestamp(CCalendar::GetMaxDate());
		}

		$evFromTS = CCalendar::Timestamp($event['DATE_FROM']);

		$limitFromTS += $event['TZ_OFFSET_FROM'];
		$limitToTS += $event['TZ_OFFSET_TO'] + CCalendar::GetDayLen();
		$limitFromTSReal = $limitFromTS;


		if ($skipTime && $length > CCalendar::GetDayLen())
		{
			$limitFromTSReal += $length - CCalendar::GetDayLen();
		}

		if ($limitFromTS < $event['DATE_FROM_TS_UTC'])
		{
			$limitFromTS = $event['DATE_FROM_TS_UTC'];
		}

		$eventDateToTsUtc = $event['DATE_TO_TS_UTC'];
		if (isset($rrule['COUNT']))
		{
			$eventDateToTsUtc = self::calculateUntilForCountRRule($event);
		}

		if ($limitToTS > $eventDateToTsUtc)
		{
			$limitToTS = $eventDateToTsUtc;
		}

		$fromTS = self::getClosestRepetitionTs($limitFromTS, $evFromTS, $rrule);

		$countOffset = 0;
		if (isset($rrule['COUNT']) && in_array($rrule['FREQ'], ['DAILY', 'MONTHLY', 'YEARLY'], true))
		{
			$lastRepetitionTs = $eventDateToTsUtc;
			if (!$skipTime)
			{
				$lastRepetitionTs -= $length;
			}

			$untilDiff = self::calculateUntilForCountRRule($event, $fromTS - $evFromTS) - $lastRepetitionTs;
			$freqDuration = self::getFreqDuration($rrule['FREQ']);
			$countOffset = (int)($untilDiff / ($freqDuration * $rrule['INTERVAL']));
		}

		if ($skipTime)
		{
			$event['~DATE_FROM'] = CCalendar::Date(CCalendar::Timestamp($event['DATE_FROM']), false);
			$event['~DATE_TO'] = CCalendar::Date(CCalendar::Timestamp($event['DATE_TO']), false);
		}
		else
		{
			$event['~DATE_FROM'] = $event['DATE_FROM'];
			$event['~DATE_TO'] = $event['DATE_TO'];
		}

		$hour = (int)date('H', $fromTS);
		$min = (int)date('i', $fromTS);
		$sec = (int)date('s', $fromTS);

		$orig_d = (int)date('d', $fromTS);
		$orig_m = (int)date('m', $fromTS);
		$orig_y = (int)date('Y', $fromTS);

		$realCount = 0;
		$dispCount = 0;

		while(true)
		{
			$d = (int)date('d', $fromTS);
			$m = (int)date('m', $fromTS);
			$y = (int)date('Y', $fromTS);
			$toTS = mktime($hour, $min, $sec + $length, $m, $d, $y);

			if (
				(isset($rrule['COUNT']) && $rrule['COUNT'] > 0 && ($realCount + $countOffset) >= $rrule['COUNT'])
				|| ($loadLimit && $dispCount >= $loadLimit)
				|| ($fromTS > $limitToTS)
				|| ($instanceCount && $dispCount >= $instanceCount)
				|| (!$fromTS || $fromTS < $evFromTS - CCalendar::GetDayLen()) // Emergency exit (mantis: 56981)
			)
			{
				break;
			}

			// Common handling
			$event['DATE_FROM'] = CCalendar::Date($fromTS, !$skipTime, false);
			$event['DATE_FROM_FORMATTED'] = self::getDateInJsFormat(
				CCalendar::createDateTimeObjectFromString($event['DATE_FROM']),
				$skipTime
			);

			$event['RRULE'] = $rrule;
			$event['RINDEX'] = $realCount > 0 || $countOffset > 0
				? $realCount + $countOffset
				: self::getFirstInstanceIndex($fromTS, $event['~DATE_FROM'])
			;

			$exclude = false;

			if (!empty($exDate))
			{
				$fromDate = CCalendar::Date($fromTS, false);
				$exclude = in_array($fromDate, $exDate, true);
			}

			if ($rrule['FREQ'] === 'WEEKLY')
			{
				$weekDay = CCalendar::WeekDayByInd(date("w", $fromTS));

				if (!empty($rrule['BYDAY'][$weekDay]))
				{
					$realCount++;

					if (($preciseLimits && $toTS >= $limitFromTSReal) || (!$preciseLimits && $toTS > $limitFromTS - $h24))
					{
						if (($event['DT_SKIP_TIME'] ?? null) === 'Y')
						{
							$toTS -= CCalendar::GetDayLen();
						}

						$event['DATE_TO'] = CCalendar::Date($toTS - ($event['TZ_OFFSET_FROM'] - $event['TZ_OFFSET_TO']), !$skipTime, false);
						$event['DATE_TO_FORMATTED'] = self::getDateInJsFormat(
							CCalendar::createDateTimeObjectFromString($event['DATE_TO']),
							$skipTime
						);

						if (!$exclude)
						{
							self::HandleEvent($res, $event, $params['userId']);
							$dispCount++;
						}
					}
				}

				if (isset($weekDay) && $weekDay === 'SU')
				{
					$delta = ($rrule['INTERVAL'] - 1) * 7 + 1;
				}
				else
				{
					$delta = 1;
				}

				$fromTS = mktime($hour, $min, $sec, $m, $d + $delta, $y);
			}
			else // DAILY, MONTHLY, YEARLY
			{
				$realCount++;

				if (($event['DT_SKIP_TIME'] ?? null) === 'Y')
				{
					$toTS -= CCalendar::GetDayLen();
				}

				if (
					($preciseLimits && $toTS >= $limitFromTSReal)
					|| (!$preciseLimits && $toTS > $limitFromTS - $h24)
				)
				{
					$event['DATE_TO'] = CCalendar::Date($toTS - ($event['TZ_OFFSET_FROM'] - $event['TZ_OFFSET_TO']), !$skipTime, false);
					$event['DATE_TO_FORMATTED'] = self::getDateInJsFormat(
						CCalendar::createDateTimeObjectFromString($event['DATE_TO']),
						$skipTime
					);

					//$event['DATE_TO'] = CCalendar::Date($toTS, !$skipTime, false);
					if (!$exclude)
					{
						self::HandleEvent($res, $event, $params['userId']);
						$dispCount++;
					}
				}

				switch ($rrule['FREQ'])
				{
					case 'DAILY':
						$fromTS = mktime($hour, $min, $sec, $m, $d + $rrule['INTERVAL'], $y);
						break;
					case 'MONTHLY':
						$durOffset = $realCount * $rrule['INTERVAL'];

						$day = $orig_d;
						$month = $orig_m + $durOffset;
						$year = $orig_y;

						if ($month > 12)
						{
							$delta_y = floor($month / 12);
							$delta_m = $month - $delta_y * 12;

							$month = $delta_m;
							$year = $orig_y + $delta_y;
						}

						// 1. Check only for 29-31 dates. 2.We are out of range in this month
						if ($orig_d > 28 && $orig_d > date("t", mktime($hour, $min, $sec, $month, 1, $year)))
						{
							$month++;
							$day = 1;
						}

						$fromTS = mktime($hour, $min, $sec, $month, $day, $year);
						break;
					case 'YEARLY':
						$fromTS = mktime($hour, $min, $sec, $orig_m, $orig_d, $y + $rrule['INTERVAL']);
						break;
				}
			}
		}
	}

	private static function getFirstInstanceIndex($fromTs, $eventFromDate): int
	{
		$eventTs = (int)CCalendar::Timestamp($eventFromDate);

		return (($fromTs - $eventTs) / 86400) > 1 ? 1 : 0;
	}

	public static function getClosestRepetitionTs(int $limitFromTS, int $evFromTS, array $rrule): int
	{
		$interval = (int)$rrule['INTERVAL'];

		$hour = (int)date('H', $evFromTS);
		$min = (int)date('i', $evFromTS);
		$sec = (int)date('s', $evFromTS);

		$orig_d = (int)date('d', $evFromTS);
		$orig_m = (int)date('m', $evFromTS);
		$orig_y = (int)date('Y', $evFromTS);

		$closestDateTs = $limitFromTS;

		$freqDuration = self::getFreqDuration($rrule['FREQ']);

		if ($rrule['FREQ'] === 'DAILY')
		{
			$dayDifference = round(($limitFromTS - $evFromTS) / $freqDuration);
			$dayDifference -= $dayDifference % $interval;
			$closestDateTs = mktime($hour, $min, $sec, $orig_m, $orig_d + $dayDifference, $orig_y);
		}

		if ($rrule['FREQ'] === 'MONTHLY')
		{
			$monthDifference = round(($limitFromTS - $evFromTS) / $freqDuration);
			$monthDifference -= $monthDifference % $interval;
			$closestDateTs = mktime($hour, $min, $sec, $orig_m + $monthDifference, $orig_d, $orig_y);
		}

		if ($rrule['FREQ'] === 'YEARLY')
		{
			$yearDifference = round(($limitFromTS - $evFromTS) / $freqDuration);
			$yearDifference -= $yearDifference % $interval;
			$closestDateTs = mktime($hour, $min, $sec, $orig_m, $orig_d, $orig_y + $yearDifference);
		}

		if ($rrule['FREQ'] === 'WEEKLY')
		{
			$dayDifference = round(($limitFromTS - $evFromTS) / $freqDuration);
			$count = round($dayDifference / (count($rrule['BYDAY']) * 7 * $interval) + 1);
			$daysCount = round(($count - 1) / count($rrule['BYDAY']) * 7 * $interval);
			$closestDateTs = mktime($hour, $min, $sec, $orig_m, $orig_d + $daysCount, $orig_y);
		}

		$closestRepetitionDate = (int)date('d', $closestDateTs);
		$closestRepetitionMonth = (int)date('m', $closestDateTs);
		$closestRepetitionYear = (int)date('Y', $closestDateTs);

		return mktime($hour, $min, $sec, $closestRepetitionMonth, $closestRepetitionDate, $closestRepetitionYear);
	}

	public static function getFreqDuration(string $freq): int
	{
		if ($freq === 'DAILY' || $freq === 'WEEKLY')
		{
			return 60 * 60 * 24;
		}

		if ($freq === 'MONTHLY')
		{
			return 60 * 60 * 24 * 30;
		}

		if ($freq === 'YEARLY')
		{
			return 60 * 60 * 24 * 365;
		}

		return 0;
	}

	public static function ParseRRULE($rule = null)
	{
		$res = [];
		if (!$rule)
		{
			return $res;
		}

		if (is_array($rule))
		{
			return isset($rule['FREQ'])
				? $rule
				: $res;
		}

		$arRule = explode(";", $rule);
		if (!is_array($arRule))
		{
			return $res;
		}

		foreach($arRule as $par)
		{
			$arPar = explode("=", $par);
			if (!empty($arPar[0]))
			{
				switch($arPar[0])
				{
					case 'FREQ':
						if (in_array($arPar[1], ['DAILY', 'WEEKLY', 'MONTHLY', 'YEARLY']))
						{
							$res['FREQ'] = $arPar[1];
						}

						break;
					case 'COUNT':
					case 'INTERVAL':
						if ((int)$arPar[1] > 0)
						{
							$res[$arPar[0]] = (int)$arPar[1];
						}

						break;
					case 'UNTIL':
						if (
							CCalendar::DFormat(false) !== ExcludedDatesCollection::EXCLUDED_DATE_FORMAT
							&& $arPar[1][2] === '.'
							&& $arPar[1][5] === '.'
						)
						{
							$arPar[1] = self::convertDateToCulture($arPar[1]);
						}
						$res['UNTIL'] = CCalendar::Timestamp($arPar[1])
							? $arPar[1]
							: CCalendar::Date((int)$arPar[1], false, false)
						;

						break;
					case 'BYDAY':
						$res[$arPar[0]] = [];
						foreach(explode(',', $arPar[1]) as $day)
						{
							$matches = [];
							if (preg_match('/((\-|\+)?\d+)?(MO|TU|WE|TH|FR|SA|SU)/', $day, $matches))
							{
								$res[$arPar[0]][$matches[3]] =
									$matches[1] === ''
										? $matches[3]
										: $matches[1];
							}
						}
						if (empty($res[$arPar[0]]))
						{
							unset($res[$arPar[0]]);
						}

						break;
					case 'BYMONTHDAY':
						$res[$arPar[0]] = [];
						foreach(explode(',', $arPar[1]) as $day)
						{
							if (abs($day) > 0 && abs($day) <= 31)
							{
								$res[$arPar[0]][(int)$day] = (int)$day;
							}
						}
						if (empty($res[$arPar[0]]))
						{
							unset($res[$arPar[0]]);
						}

						break;
					case 'BYYEARDAY':
					case 'BYSETPOS':
						$res[$arPar[0]] = [];
						foreach(explode(',', $arPar[1]) as $day)
						{
							if (abs($day) > 0 && abs($day) <= 366)
							{
								$res[$arPar[0]][(int)$day] = (int)$day;
							}
						}
						if (empty($res[$arPar[0]]))
						{
							unset($res[$arPar[0]]);
						}

						break;
					case 'BYWEEKNO':
						$res[$arPar[0]] = [];
						foreach(explode(',', $arPar[1]) as $day)
						{
							if (abs($day) > 0 && abs($day) <= 53)
							{
								$res[$arPar[0]][(int)$day] = (int)$day;
							}
						}
						if (empty($res[$arPar[0]]))
						{
							unset($res[$arPar[0]]);
						}

						break;
					case 'BYMONTH':
						$res[$arPar[0]] = [];
						foreach(explode(',', $arPar[1]) as $m)
						{
							if ($m > 0 && $m <= 12)
							{
								$res[$arPar[0]][(int)$m] = (int)$m;
							}
						}
						if (empty($res[$arPar[0]]))
						{
							unset($res[$arPar[0]]);
						}

						break;
				}
			}
		}

		if (
			$res['FREQ'] === 'WEEKLY'
			&& (
				empty($res['BYDAY'])
				|| !is_array($res['BYDAY'])
			)
		)
		{
			$res['BYDAY'] = ['MO' => 'MO'];
		}

		if ($res['FREQ'] !== 'WEEKLY' && isset($res['BYDAY']))
		{
			unset($res['BYDAY']);
		}

		$res['INTERVAL'] = (int)($res['INTERVAL'] ?? null);
		if ($res['INTERVAL'] <= 1)
		{
			$res['INTERVAL'] = 1;
		}

		$res['~UNTIL'] = $res['UNTIL'] ?? null;
		if (($res['UNTIL'] ?? null) === CCalendar::GetMaxDate())
		{
			$res['~UNTIL'] = '';
		}

		$res['UNTIL_TS'] = !empty($res['UNTIL']) && is_string($res['UNTIL'])
			? CCalendar::TimestampUTC($res['UNTIL'])
			: null
		;

		return $res;
	}

	/**
	 * @param $exDate
	 * @return array|false|string[]
	 */
	public static function GetExDate($exDate = '')
	{
		$result = [];
		if (is_string($exDate))
		{
			$result = $exDate === '' ? [] : explode(';', $exDate);
		}

		return $result ?: [];
	}

	private static function HandleEvent(&$res, $event = [], $userId = null)
	{
		$userId = $userId ?: CCalendar::GetCurUserId();

		$res[] = self::calculateUserOffset($userId, $event);
	}

	private static function calculateUserOffset($userId, $event = [])
	{
		if (($event['DT_SKIP_TIME'] ?? null) === 'N')
		{
			$currentUserTimezone = \CCalendar::GetUserTimezoneName($userId);

			$fromTs = \CCalendar::Timestamp($event['DATE_FROM']);
			$toTs = $fromTs + ($event['DT_LENGTH'] ?? null);

			$event['~USER_OFFSET_FROM'] = CCalendar::GetTimezoneOffset(($event['TZ_FROM'] ?? null), $fromTs)
				- \CCalendar::GetTimezoneOffset($currentUserTimezone, $fromTs);

			$event['~USER_OFFSET_TO'] = CCalendar::GetTimezoneOffset(($event['TZ_TO'] ?? null), $toTs)
				- \CCalendar::GetTimezoneOffset($currentUserTimezone, $toTs);
		}
		else
		{
			$event['~USER_OFFSET_FROM'] = 0;
			$event['~USER_OFFSET_TO'] = 0;
		}

		return $event;
	}

	public static function CheckFields(&$arFields, $currentEvent = [], $userId = false)
	{
		$arFields['ID'] = (int)($arFields['ID'] ?? null);
		$arFields['PARENT_ID'] = (int)($arFields['PARENT_ID'] ?? 0);
		$arFields['OWNER_ID'] = (int)($arFields['OWNER_ID'] ?? 0);

		if (!isset($arFields['TIMESTAMP_X']))
		{
			$arFields['TIMESTAMP_X'] = CCalendar::Date(time(), true, false);
		}

		if (!$userId)
		{
			$userId = CCalendar::GetCurUserId();
		}

		if (!isset($arFields['DT_SKIP_TIME']) && isset($currentEvent['DT_SKIP_TIME']))
		{
			$arFields['DT_SKIP_TIME'] = $currentEvent['DT_SKIP_TIME'];
		}
		if (!isset($arFields['DATE_FROM']) && isset($currentEvent['DATE_FROM']))
		{
			$arFields['DATE_FROM'] = $currentEvent['DATE_FROM'];
		}
		if (!isset($arFields['DATE_TO']) && isset($currentEvent['DATE_TO']))
		{
			$arFields['DATE_TO'] = $currentEvent['DATE_TO'];
		}

		$isNewEvent = !isset($arFields['ID']) || $arFields['ID'] <= 0;
		if (!isset($arFields['DATE_CREATE']) && $isNewEvent)
		{
			$arFields['DATE_CREATE'] = $arFields['TIMESTAMP_X'];
		}

		// Skip time
		if (isset($arFields['SKIP_TIME']))
		{
			$arFields['DT_SKIP_TIME'] = $arFields['SKIP_TIME'] ? 'Y' : 'N';
			unset($arFields['SKIP_TIME']);
		}
		elseif (isset($arFields['DT_SKIP_TIME']) && $arFields['DT_SKIP_TIME'] !== 'Y' && $arFields['DT_SKIP_TIME'] !== 'N')
		{
			unset($arFields['DT_SKIP_TIME']);
		}

		unset($arFields['DT_FROM'], $arFields['DT_TO']);

		$arFields['DT_SKIP_TIME'] = ($arFields['DT_SKIP_TIME'] ?? null) !== 'Y' ? 'N' : 'Y';
		$fromTs = CCalendar::Timestamp($arFields['DATE_FROM'], false, $arFields['DT_SKIP_TIME'] !== 'Y');
		$toTs = CCalendar::Timestamp($arFields['DATE_TO'], false, $arFields['DT_SKIP_TIME'] !== 'Y');

		$arFields['DATE_FROM'] = CCalendar::Date($fromTs);
		$arFields['DATE_TO'] = CCalendar::Date($toTs);

		if (!$fromTs)
		{
			$arFields['DATE_FROM'] = FormatDate("SHORT", time());
			$fromTs = CCalendar::Timestamp($arFields['DATE_FROM'], false, false);
			if (!$toTs)
			{
				$arFields['DATE_TO'] = $arFields['DATE_FROM'];
				$toTs = $fromTs;
				$arFields['DT_SKIP_TIME'] = 'Y';
			}
		}
		elseif (!$toTs)
		{
			$arFields['DATE_TO'] = $arFields['DATE_FROM'];
			$toTs = $fromTs;
		}

		if (($arFields['DT_SKIP_TIME'] ?? null) !== 'Y')
		{
			$arFields['DT_SKIP_TIME'] = 'N';
			if (!isset($arFields['TZ_FROM']) && isset($currentEvent['TZ_FROM']))
			{
				$arFields['TZ_FROM'] = $currentEvent['TZ_FROM'];
			}
			if (!isset($arFields['TZ_TO']) && isset($currentEvent['TZ_TO']))
			{
				$arFields['TZ_TO'] = $currentEvent['TZ_TO'];
			}

			if (!isset($arFields['TZ_FROM']) && !isset($arFields['TZ_TO']))
			{
				$userTimezoneName = CCalendar::GetUserTimezoneName($userId, true);
				$arFields['TZ_FROM'] = $userTimezoneName;
				$arFields['TZ_TO'] = $userTimezoneName;
			}

			if (!isset($arFields['TZ_OFFSET_FROM']))
			{
				$arFields['TZ_OFFSET_FROM'] = CCalendar::GetTimezoneOffset($arFields['TZ_FROM'], $fromTs);
			}
			if (!isset($arFields['TZ_OFFSET_TO']))
			{
				$arFields['TZ_OFFSET_TO'] = CCalendar::GetTimezoneOffset($arFields['TZ_TO'], $toTs);
			}
		}

		if (!isset($arFields['TZ_OFFSET_FROM']))
		{
			$arFields['TZ_OFFSET_FROM'] = 0;
		}
		if (!isset($arFields['TZ_OFFSET_TO']))
		{
			$arFields['TZ_OFFSET_TO'] = 0;
		}

		if (!isset($arFields['DATE_FROM_TS_UTC']))
		{
			$arFields['DATE_FROM_TS_UTC'] = $fromTs - $arFields['TZ_OFFSET_FROM'];
		}
		if (!isset($arFields['DATE_TO_TS_UTC']))
		{
			$arFields['DATE_TO_TS_UTC'] = $toTs - $arFields['TZ_OFFSET_TO'];
		}

		if ($arFields['DATE_FROM_TS_UTC'] > $arFields['DATE_TO_TS_UTC'])
		{
			$arFields['DATE_TO'] = $arFields['DATE_FROM'];
			$arFields['DATE_TO_TS_UTC'] = $arFields['DATE_FROM_TS_UTC'];
			$arFields['TZ_OFFSET_TO'] = $arFields['TZ_OFFSET_FROM'];
			$arFields['TZ_TO'] = $arFields['TZ_FROM'];
		}

		$h24 = 60 * 60 * 24; // 24 hours
		if (($arFields['DT_SKIP_TIME'] ?? null) === 'Y')
		{
			unset($arFields['TZ_FROM'], $arFields['TZ_TO'], $arFields['TZ_OFFSET_FROM'], $arFields['TZ_OFFSET_TO']);
		}

		// Event length in seconds
		if ((int)$fromTs === (int)$toTs && date('H:i', $fromTs) === '00:00' && $arFields['DT_SKIP_TIME'] === 'Y') // One day
		{
			$arFields['DT_LENGTH'] = $h24;
		}
		else
		{
			$arFields['DT_LENGTH'] = (int)($arFields['DATE_TO_TS_UTC'] - $arFields['DATE_FROM_TS_UTC']);
			if (($arFields['DT_SKIP_TIME'] ?? null) === "Y") // We have dates without times
			{
				$arFields['DT_LENGTH'] += $h24;
			}
		}

		if (empty($arFields['VERSION']))
		{
			$arFields['VERSION'] = 1;
		}

		// Accessibility
		$arFields['ACCESSIBILITY'] = mb_strtolower(trim($arFields['ACCESSIBILITY'] ?? ''));
		if (!in_array($arFields['ACCESSIBILITY'], ['busy', 'quest', 'free', 'absent'], true))
		{
			$arFields['ACCESSIBILITY'] = 'busy';
		}

		// Importance
		$arFields['IMPORTANCE'] = mb_strtolower(trim($arFields['IMPORTANCE'] ?? ''));
		if (!in_array($arFields['IMPORTANCE'], ['high', 'normal', 'low']))
		{
			$arFields['IMPORTANCE'] = 'normal';
		}

		// Color
		$arFields['COLOR'] = CCalendar::Color($arFields['COLOR'] ?? null, false);

		// Section
		if (
            isset($arFields['SECTIONS'])
            && !is_array($arFields['SECTIONS'])
            && (int)$arFields['SECTIONS'] > 0
        )
		{
			$arFields['SECTIONS'] = (array)((int)($arFields['SECTIONS'] ?? null));
		}

		self::checkRecurringRuleField($arFields, $toTs, ($currentEvent['EXDATE'] ?? null));

		// Location
		if (!isset($arFields['LOCATION']) || !is_array($arFields['LOCATION']))
		{
			$arFields['LOCATION'] = [
                "NEW" => is_string($arFields['LOCATION'] ?? null)
                    ? $arFields['LOCATION']
                    : "",
            ];
		}

		// Private
		$arFields['PRIVATE_EVENT'] = isset($arFields['PRIVATE_EVENT']) && $arFields['PRIVATE_EVENT'];

		return true;
	}

	public static function CheckEntryChanges($newFields = [], $currentFields = [])
	{
		$changes = [];
		$fieldList = [
			'NAME',
			'DATE_FROM',
			'DATE_TO',
			'RRULE',
			'DESCRIPTION',
			'LOCATION',
			'IMPORTANCE',
		];

		foreach ($fieldList as $fieldKey)
		{
			if ($fieldKey === 'LOCATION')
			{
				if (
					is_array($newFields[$fieldKey] ?? null)
					&& ($newFields[$fieldKey]['NEW'] ?? null) !== ($currentFields[$fieldKey] ?? null)
					&& (CCalendar::GetTextLocation($newFields[$fieldKey]['NEW'] ?? '')) !== (CCalendar::GetTextLocation($currentFields[$fieldKey] ?? ''))
				)
				{
					$changes[] = [
						'fieldKey' => $fieldKey,
						'oldValue' => $currentFields[$fieldKey] ?? null,
						'newValue' => $newFields[$fieldKey]['NEW'] ?? null,
					];
				}
				else if (
					!is_array($newFields[$fieldKey] ?? null)
					&& ($newFields[$fieldKey] ?? null) !== ($currentFields[$fieldKey] ?? null)
					&& (CCalendar::GetTextLocation($newFields[$fieldKey] ?? '')) !== (CCalendar::GetTextLocation($currentFields[$fieldKey] ?? ''))
				)
				{
					$changes[] = [
						'fieldKey' => $fieldKey,
						'oldValue' => $currentFields[$fieldKey],
						'newValue' => $newFields[$fieldKey],
					];
				}
			}
			else if ($fieldKey === 'DATE_FROM')
			{
				if (
					$newFields[$fieldKey] !== $currentFields[$fieldKey]
					|| ($newFields['TZ_FROM'] ?? null) !== ($currentFields['TZ_FROM'] ?? null)
				)
				{
					$changes[] = [
						'fieldKey' => $fieldKey,
						'oldValue' => $currentFields[$fieldKey],
						'newValue' => $newFields[$fieldKey],
					];
				}
			}
			else if ($fieldKey === 'DATE_TO')
			{
				if (
					(
						$newFields['DATE_FROM'] === $currentFields['DATE_FROM']
						&& ($newFields['TZ_FROM'] ?? null) === ($currentFields['TZ_FROM'] ?? null)
					)
					&&
					(
						$newFields[$fieldKey] !== $currentFields[$fieldKey]
						|| ($newFields['TZ_TO'] ?? null) !== ($currentFields['TZ_TO'] ?? null)
					)
				)
				{
					$changes[] = [
						'fieldKey' => $fieldKey,
						'oldValue' => $currentFields[$fieldKey],
						'newValue' => $newFields[$fieldKey],
					];
				}
			}
			else if ($fieldKey === 'IMPORTANCE')
			{
				if (
					$newFields[$fieldKey] !== $currentFields[$fieldKey]
					&& $newFields[$fieldKey] === 'high'
				)
				{
					$changes[] = [
						'fieldKey' => $fieldKey,
						'oldValue' => $currentFields[$fieldKey],
						'newValue' => $newFields[$fieldKey],
					];
				}
			}
			else if ($fieldKey === 'DESCRIPTION')
			{
				if (mb_strtolower(trim($newFields[$fieldKey])) !== mb_strtolower(trim($currentFields[$fieldKey])))
				{
					$changes[] = [
						'fieldKey' => $fieldKey,
						'oldValue' => $currentFields[$fieldKey],
						'newValue' => $newFields[$fieldKey],
					];
				}
			}
			else if ($fieldKey === 'RRULE')
			{
				$newRule = self::ParseRRULE($newFields[$fieldKey] ?? null);
				$oldRule = self::ParseRRULE($currentFields[$fieldKey] ?? null);

				if (
                    (($newRule['FREQ'] ?? null) !== ($oldRule['FREQ'] ?? null))
					|| (($newRule['INTERVAL'] ?? null) !== ($oldRule['INTERVAL'] ?? null))
					|| (($newRule['BYDAY'] ?? null) !== ($oldRule['BYDAY'] ?? null))
				)
				{
					$changes[] = [
						'fieldKey' => $fieldKey,
						'oldValue' => $oldRule,
						'newValue' => $newRule,
					];
				}
			}

			else if (($newFields[$fieldKey] ?? null) !== ($currentFields[$fieldKey] ?? null))
			{
				$changes[] = [
					'fieldKey' => $fieldKey,
					'oldValue' => $currentFields[$fieldKey],
					'newValue' => $newFields[$fieldKey],
				];
			}
		}

		if (
			is_array($newFields['ATTENDEES_CODES'] ?? null)
			&& is_array($currentFields['ATTENDEES_CODES'] ?? null)
			&& (count(array_diff($newFields['ATTENDEES_CODES'], $currentFields['ATTENDEES_CODES']))
				|| count(array_diff($currentFields['ATTENDEES_CODES'], $newFields['ATTENDEES_CODES'])))
		)
		{
			$changes[] = [
				'fieldKey' => 'ATTENDEES',
				'oldValue' => $currentFields['ATTENDEES_CODES'],
				'newValue' => $newFields['ATTENDEES_CODES'],
			];
		}

		return $changes;
	}

	private static function PackRRule($RRule = [])
	{
		$strRes = "";
		if (is_array($RRule))
		{
			foreach($RRule as $key => $val)
			{
				$strRes .= $key . '=' . $val . ';';
			}
		}

		return trim($strRes, ', ');
	}

	//

	/**
	 * @throws \Bitrix\Main\NotImplementedException
	 * @throws \Bitrix\Main\LoaderException
	 * @throws \Bitrix\Main\SystemException
	 * @throws \Bitrix\Main\ObjectException
	 * @throws \Bitrix\Main\ObjectPropertyException
	 * @throws \Bitrix\Main\ArgumentException
	 */
	private static function CreateChildEvents($parentId, $arFields, $params, $changeFields)
	{
		global $CACHE_MANAGER;
		$parentId = (int)$parentId;
		$isNewEvent = !isset($arFields['ID']) || $arFields['ID'] <= 0;
		$chatId = (int) ($arFields['~MEETING']['CHAT_ID'] ?? null) ;
		$involvedAttendees = []; // List of all attendees to invite or to exclude from event
		$isCalDavEnabled = CCalendar::IsCalDAVEnabled();
		$isPastEvent = ($arFields['DATE_TO_TS_UTC'] ?? null) < (time() - (int)date('Z'));

		$userId = $params['userId'];
		$attendees = is_array($arFields['ATTENDEES'] ?? null) ? $arFields['ATTENDEES'] : []; // List of attendees for event
		$chat = null;
		$eventWithEmailGuestEnabled = Bitrix24Manager::isFeatureEnabled(FeatureDictionary::CALENDAR_EVENTS_WITH_EMAIL_GUESTS);

		unset($params['dontSyncParent']);

		if ($chatId > 0 && Loader::includeModule('im'))
		{
			$chat = new \CIMChat($userId);
		}

		if (empty($attendees) && !($arFields['CAL_TYPE'] === 'user' && $arFields['OWNER_ID'] === $userId))
		{
			$attendees[] = (int)$arFields['CREATED_BY'];
		}

		foreach($attendees as $userKey)
		{
			$involvedAttendees[] = (int)$userKey;
		}

		$currentAttendeesIndex = [];
		$deletedAttendees = [];
		$affectedEventIds = [$parentId];
		if (!$isNewEvent)
		{
			$curAttendees = self::GetAttendees($parentId);
			$curAttendees = is_array(($curAttendees[$parentId] ?? null)) ? $curAttendees[$parentId] : [];
			foreach($curAttendees as $user)
			{
				$currentAttendeesIndex[$user['USER_ID']] = $user;
				if (
					(int)$user['USER_ID'] !== $arFields['MEETING_HOST']
					&& (
						(int)$user['USER_ID'] !== (int)$arFields['OWNER_ID']
						|| $arFields['CAL_TYPE'] !== 'user'
					)
				)
				{
					$deletedAttendees[$user['USER_ID']] = (int)$user['USER_ID'];
					$involvedAttendees[] = (int)$user['USER_ID'];
				}
			}
		}
		$involvedAttendees = array_unique($involvedAttendees);
		$userIndex = self::generateUserIndex($involvedAttendees, $arFields['MEETING_HOST']);

		$attendeeStatuses = self::getAttendeeStatuses(
			$attendees,
			$arFields,
			$params,
			$currentAttendeesIndex,
			$isNewEvent
		);
		$params['attendeeStatuses'] = $attendeeStatuses;

		foreach ($attendees as $userKey)
		{
			$clonedParams = $params;
			$attendeeId = (int)$userKey;
			$isNewAttendee = !empty($clonedParams['currentEvent']['ATTENDEE_LIST'])
				&& is_array($clonedParams['currentEvent']['ATTENDEE_LIST'])
				&& self::isNewAttendee($clonedParams['currentEvent']['ATTENDEE_LIST'], $attendeeId)
			;
			$CACHE_MANAGER->ClearByTag('calendar_user_'.$attendeeId);

			// Skip creation of child event if it's event inside his own user calendar
			if (
				$attendeeId
				&& (($arFields['CAL_TYPE'] ?? null) !== 'user' || (int)($arFields['OWNER_ID'] ?? null) !== $attendeeId)
			)
			{
				$childParams = $clonedParams;
				$childParams['arFields']['CAL_TYPE'] = 'user';
				$childParams['arFields']['PARENT_ID'] = $parentId;
				$childParams['arFields']['OWNER_ID'] = $attendeeId;
				$childParams['arFields']['CREATED_BY'] = $attendeeId;
				$childParams['arFields']['CREATED'] = $arFields['DATE_CREATE'] ?? null;
				$childParams['arFields']['MODIFIED'] = $arFields['TIMESTAMP_X'] ?? null;
				$childParams['arFields']['ACCESSIBILITY'] = $arFields['ACCESSIBILITY'] ?? null;
				$childParams['arFields']['MEETING'] = $arFields['~MEETING'] ?? null;
				$childParams['arFields']['TEXT_LOCATION'] = CCalendar::GetTextLocation($arFields["LOCATION"] ?? null);
				$childParams['arFields']['MEETING_STATUS'] = $attendeeStatuses[$attendeeId]['status'];
				$childParams['arFields']['EVENT_TYPE'] = $arFields['EVENT_TYPE'] ?? null;
				$childParams['sendInvitations'] = $clonedParams['sendInvitations'] ?? null;

				unset(
					$childParams['arFields']['SECTIONS'],
					$childParams['arFields']['SECTION_ID'],
					$childParams['currentEvent'],
					$childParams['updateReminders'],
					$childParams['arFields']['ID'],
					$childParams['arFields']['DAV_XML_ID'],
					$childParams['arFields']['G_EVENT_ID'],
					$childParams['arFields']['SYNC_STATUS']
				);

				$isExchangeEnabled = CCalendar::IsExchangeEnabled($attendeeId);

				if (
					isset($userIndex[$attendeeId])
					&& $userIndex[$attendeeId]['EXTERNAL_AUTH_ID'] === 'email'
					&& $isNewEvent
					&& !$eventWithEmailGuestEnabled
				)
				{
					// Just skip external emil users if they are not allowed
					// We will show warning on the client's side
					continue;
				}

				if (!empty($currentAttendeesIndex[$attendeeId]))
				{
					$childParams['arFields']['ID'] = $currentAttendeesIndex[$attendeeId]['EVENT_ID'];

					if (empty($arFields['~MEETING']['REINVITE']))
					{
						$childParams['arFields']['MEETING_STATUS'] = $currentAttendeesIndex[$attendeeId]['STATUS'];

						$childParams['sendInvitations'] = $childParams['sendInvitations'] &&  $currentAttendeesIndex[$attendeeId]['STATUS'] !== 'Q';
					}

					if ($attendeeStatuses[$attendeeId]['sendInvitations'])
					{
						$childParams['sendInvitations'] = true;
					}

					if (
						($isExchangeEnabled || $isCalDavEnabled)
						&& ($childParams['overSaving'] ?? false) !== true
					)
					{
						self::prepareArFieldBeforeSyncEvent($childParams);
						$childParams['currentEvent'] = self::GetById($childParams['arFields']['ID'], false);

						$davParams = [
							'bCalDav' => $isCalDavEnabled,
							'bExchange' => $isExchangeEnabled,
							'sectionId' => (int)$childParams['currentEvent']['SECTION_ID'],
							'modeSync' => $clonedParams['modeSync'],
							'editInstance' => $clonedParams['editInstance'],
							'originalDavXmlId' => $childParams['currentEvent']['G_EVENT_ID'],
							'instanceTz' => $childParams['currentEvent']['TZ_FROM'],
							'editParentEvents' => $clonedParams['editParentEvents'],
							'editNextEvents' => $clonedParams['editNextEvents'],
							'syncCaldav' => $clonedParams['syncCaldav'],
							'parentDateFrom' => $childParams['currentEvent']['DATE_FROM'],
							'parentDateTo' => $childParams['currentEvent']['DATE_TO'],
						];
						CCalendarSync::DoSaveToDav($childParams['arFields'], $davParams, $childParams['currentEvent']);
					}
				}
				else
				{
					$childSectId = CCalendar::GetMeetingSection($attendeeId, true);
					if ($childSectId)
					{
						$childParams['arFields']['SECTIONS'] = [$childSectId];
					}

					if (!$clonedParams['editInstance'])
					{
						$childParams['arFields']['DAV_XML_ID'] = self::getUidForChildEvent($childParams['arFields']);
					}

					$parentEvent = [];
					if (!empty($childParams['arFields']['RECURRENCE_ID']))
					{
						$parentEvent = Internals\EventTable::query()
							->where('PARENT_ID' , (int)$childParams['arFields']['RECURRENCE_ID'])
							->where('OWNER_ID' , (int)($childParams['arFields']['OWNER_ID'] ?? 0))
							->setSelect([
								'DAV_XML_ID',
								'TZ_FROM',
								'DATE_FROM',
								'DATE_TO',
							])
							->setLimit(1)
							->exec()
							->fetch() ?: [];
					}

					if ($parentEvent)
					{
						$childParams['arFields']['DAV_XML_ID'] = $parentEvent['DAV_XML_ID'] ?? null;
					}
					else
					{
						unset(
							$childParams['arFields']['ORIGINAL_DATE_FROM'],
							$childParams['arFields']['RECURRENCE_ID'],
							$clonedParams['recursionEditMode']
						);

						$childParams['arFields']['DAV_XML_ID'] = UidGenerator::createInstance()
							->setPortalName(Util::getServerName())
							->setDate(new Date(Util::getDateObject(
								$childParams['arFields']['DATE_FROM'] ?? null,
								false,
								($childParams['arFields']['TZ_FROM'] ?? null) ?: null
							)))
							->setUserId((int)($childParams['arFields']['OWNER_ID'] ?? null))
							->getUidWithDate();
					}


					// CalDav & Exchange
					if (
						($isExchangeEnabled || $isCalDavEnabled)
						&& ($childParams['overSaving'] ?? false) !== true
					)
					{
						$davParams = [
							'bCalDav' => $isCalDavEnabled,
							'bExchange' => $isExchangeEnabled,
							'sectionId' => $childSectId,
							'modeSync' => $clonedParams['modeSync'] ?? null,
							'editInstance' => $clonedParams['editInstance'] ?? null,
							'instanceTz' => $parentEvent['TZ_FROM'] ?? null,
							'editParentEvents' => $clonedParams['editParentEvents'] ?? null,
							'editNextEvents' => $clonedParams['editNextEvents'] ?? null,
							'syncCaldav' => $clonedParams['syncCaldav'] ?? null,
							'parentDateFrom' => $parentEvent['DATE_FROM'] ?? null,
							'parentDateTo' => $parentEvent['DATE_TO'] ?? null,
						];
						CCalendarSync::DoSaveToDav($childParams['arFields'], $davParams);
					}
				}

				$curEvent = null;
				if (!empty($childParams['arFields']['ID']))
				{
					$curEvent = self::GetList([
						'arFilter' => [
							"ID" => (int)$childParams['arFields']['ID'],
							"DELETED" => 'N',
						],
						'checkPermissions' => false,
						'parseRecursion' => false,
						'fetchAttendees' => true,
						'fetchMeetings' => false,
						'userId' => $userId,
					]);
				}
				if ($curEvent)
				{
					$curEvent = $curEvent[0];
				}

				if (!empty($curEvent['COLOR']))
				{
					if (
						empty($childParams['arFields']['COLOR'])
						|| ($curEvent['COLOR'] !== $childParams['arFields']['COLOR'])
					)
					{
						$childParams['arFields']['COLOR'] = $curEvent['COLOR'];
					}
				}

				$id = self::Edit($childParams);
				$affectedEventIds[] = $id;

				if (
					$userIndex[$attendeeId]
					&& $userIndex[$attendeeId]['EXTERNAL_AUTH_ID'] === 'email'
					&& ((!($clonedParams['fromWebservice'] ?? false)) || !empty($changeFields))
					&& !$isPastEvent
					&& ($childParams['overSaving'] ?? false) !== true
				)
				{

					$sender = self::getSenderForIcal($userIndex, $childParams['arFields']['MEETING_HOST']);

					if (empty($sender) || !$sender['ID'])
					{
						continue;
					}

					if (!empty($email = self::getSenderEmailForIcal($arFields['MEETING'])) && !self::$isAddIcalFailEmailError)
					{
						$sender['EMAIL'] = $email;
					}
					else
					{
						CCalendar::ThrowError(GetMessage("EC_ICAL_NOTICE_DO_NOT_SET_EMAIL"));
						self::$isAddIcalFailEmailError = true;
						continue;
					}

					$arFields['ID'] = $id;
					$invitationInfo = [];

					if (!empty($currentAttendeesIndex[$attendeeId]))
					{
						$mailChangeFields = array_filter($changeFields,
							static fn (array $field) => !in_array(
								$field['fieldKey'],
								['ATTENDEES', 'IMPORTANCE'],
								true,
							),
						);
						if (!empty($mailChangeFields))
						{
							$invitationInfo = (new InvitationInfo(
								(int)$arFields['ID'],
								(int)$sender['ID'],
								(int)$attendeeId,
								InvitationInfo::TYPE_EDIT,
								$mailChangeFields,
							))->toArray();
						}
					}
					else
					{
						$invitationInfo = (new InvitationInfo(
							(int)$arFields['ID'],
							(int)$sender['ID'],
							(int)$attendeeId,
							InvitationInfo::TYPE_REQUEST
						))->toArray();
					}

					SendingEmailNotification::sendMessageToQueue($invitationInfo);
				}

				if (
					$chatId > 0
					&& $chat
					&& $isNewAttendee
					&& $userIndex[$attendeeId]
					&& $userIndex[$attendeeId]['EXTERNAL_AUTH_ID'] !== 'email'
					&& $userIndex[$attendeeId]['EXTERNAL_AUTH_ID'] !== Sharing\SharingUser::EXTERNAL_AUTH_ID
					&& $childParams['arFields']['MEETING_STATUS'] !== 'N'
				)
				{
					$chat->AddUser($chatId, $attendeeId, $hideHistory = true, $skipMessage = false);
				}

				if ($id)
				{
					CCalendar::syncChange($id, $childParams['arFields'], $clonedParams, $curEvent);
				}

				unset($deletedAttendees[$attendeeId]);
			}
		}

		// Delete
		$eventIdToDelete = [];
		if (!$isNewEvent && !empty($deletedAttendees))
		{
			$isSharing = in_array(
				$arFields['EVENT_TYPE'] ?? '', Sharing\SharingEventManager::getSharingEventTypes(), true
			);
			if ($isSharing)
			{
				$notifyUserId = $userId;
			}
			else
			{
				$notifyUserId = $arFields['MEETING_HOST'] ?? null;
			}
			foreach($deletedAttendees as $attendeeId)
			{
				if ($chatId > 0 && $chat)
				{
					$chat->DeleteUser($chatId, $attendeeId, false);
				}

				$att = $currentAttendeesIndex[$attendeeId];
				if (
					($params['sendInvitations'] ?? null) !== false
					&& ($att['STATUS'] ?? null) === 'Y'
					&& !$isPastEvent
				)
				{
					$CACHE_MANAGER->ClearByTag('calendar_user_'.$att["USER_ID"]);
					$fromTo = self::GetEventFromToForUser($arFields, $att["USER_ID"]);
					CCalendarNotify::Send([
						'mode' => 'cancel',
						"name" => $arFields['NAME'] ?? null,
						"from" => $fromTo['DATE_FROM'] ?? null,
						"to" => $fromTo['DATE_TO'] ?? null,
						"location" => CCalendar::GetTextLocation($arFields["LOCATION"] ?? null),
						"guestId" => $att["USER_ID"] ?? null,
						"eventId" => $parentId,
						"userId" => $notifyUserId,
						"fields" => $arFields,
					]);
				}
				//add pull event to update calendar grid after event delete
				$pullUserId = (int)$attendeeId;
				if (
					$pullUserId > 0
					&& self::$sendPush
				)
				{
					Util::addPullEvent(
						PushCommand::DeleteEvent,
						$pullUserId,
						[
							'fields' => $arFields,
							'requestUid' => $params['userId'],
						]
					);
				}
				CCalendarNotify::ClearNotifications($arFields['PARENT_ID'], $pullUserId);
				$affectedEventIds[] = $att['EVENT_ID'] ?? 0;

				if ((int)($att['EVENT_ID'] ?? null))
				{
					$eventIdToDelete[] = (int)$att['EVENT_ID'];
				}

				$currentEvent = self::GetList([
					'arFilter' => [
						"PARENT_ID" => $parentId,
						"OWNER_ID" => $attendeeId,
						"IS_MEETING" => 1,
						"DELETED" => "N",
					],
					'parseRecursion' => false,
					'fetchAttendees' => true,
					'fetchMeetings' => true,
					'checkPermissions' => false,
					'setDefaultLimit' => false,
				]);
				$currentEvent = $currentEvent[0];

				$isExchangeEnabled = CCalendar::IsExchangeEnabled($attendeeId);
				if (($isExchangeEnabled || $isCalDavEnabled) && $currentEvent)
				{
					CCalendarSync::DoDeleteToDav([
						'bCalDav' => $isCalDavEnabled,
						'bExchangeEnabled' => $isExchangeEnabled,
						'sectionId' => $currentEvent['SECT_ID'] ?? null,
					], $currentEvent);
				}

				if ($currentEvent)
				{
					self::onEventDelete($currentEvent, $params);
				}

				if (isset($att['EXTERNAL_AUTH_ID']) && $att['EXTERNAL_AUTH_ID'] === 'email' && !$isPastEvent)
				{
					if (empty($receiver['EMAIL']))
					{
						continue;
					}

					$sender = self::getSenderForIcal($currentAttendeesIndex, $arFields['MEETING_HOST']);
					if ($email = self::getSenderEmailForIcal($arFields['MEETING']))
					{
						$sender['EMAIL'] = $email;
					}
					else
					{
						$meetingHostSettings = UserSettings::get($arFields['MEETING_HOST']);
						$sender['EMAIL'] = $meetingHostSettings['sendFromEmail'];
					}
					if (empty($sender['ID']) && isset($sender['USER_ID']))
					{
						$sender['ID'] = (int)$sender['USER_ID'];
					}

					$invitationInfo = (new InvitationInfo(
						(int)$arFields['ID'],
						(int)$sender['ID'],
						(int)$receiver['ID'],
						InvitationInfo::TYPE_CANCEL
					))->toArray();

					SendingEmailNotification::sendMessageToQueue($invitationInfo);
				}
			}
		}


		if (!empty($eventIdToDelete))
		{
			Internals\EventTable::updateByFilter(
				[
					'ID' => $eventIdToDelete,
					'=PARENT_ID' => (int)$parentId,
				],
				['DELETED' => 'Y'],
			);

			self::safeDeleteEventAttendees($parentId, $deletedAttendees);
		}

		if (!empty($involvedAttendees))
		{
			$involvedAttendees = array_unique($involvedAttendees);
			CCalendar::UpdateCounter($involvedAttendees, array_unique($affectedEventIds));
		}
	}

	public static function UpdateParentEventExDate($recurrenceId, $exDate, $attendeeIds)
	{
		global $CACHE_MANAGER;

		$event = Internals\EventTable::query()
			->setSelect(['PARENT_ID', 'EXDATE'])
			->where('PARENT_ID', $recurrenceId)
			->setLimit(1)
			->exec()->fetch()
		;
		$exDates = self::GetExDate($event['EXDATE']);
		$exDates[] = date(
			ExcludedDatesCollection::EXCLUDED_DATE_FORMAT,
			\CCalendar::Timestamp($exDate)
		);
		$exDates = array_unique($exDates);
		$strExDates = implode(';', $exDates);

		Internals\EventTable::updateByFilter(
			['=PARENT_ID' => (int)$recurrenceId],
			['EXDATE' => $strExDates],
		);

		if (is_array($attendeeIds))
		{
			foreach ($attendeeIds as $id)
			{
				$CACHE_MANAGER->ClearByTag('calendar_user_' . $id);
			}
		}

		return true;
	}

	public static function GetEventFromToForUser($params, $userId)
	{
		$skipTime = $params['DT_SKIP_TIME'] !== 'N';

		$fromTs = CCalendar::Timestamp($params['DATE_FROM'], false, !$skipTime);
		$toTs = CCalendar::Timestamp($params['DATE_TO'], false, !$skipTime);

		if (!$skipTime)
		{
			$fromTs -= (CCalendar::GetTimezoneOffset($params['TZ_FROM']) - CCalendar::GetCurrentOffsetUTC($userId));
			$toTs -= (CCalendar::GetTimezoneOffset($params['TZ_TO']) - CCalendar::GetCurrentOffsetUTC($userId));
		}

		$dateFrom = CCalendar::Date($fromTs, !$skipTime);
		$dateTo = CCalendar::Date($toTs, !$skipTime);

		return array(
			"DATE_FROM" => $dateFrom,
			"DATE_TO" => $dateTo,
			"TS_FROM" => $fromTs,
			"TS_TO" => $toTs,
		);
	}

	public static function OnPullPrepareArFields($arFields = [])
	{
		$arFields['~DESCRIPTION'] = self::ParseText($arFields['DESCRIPTION']);

		$arFields['~LOCATION'] = '';
		if (($arFields['LOCATION'] ?? null) !== '')
		{
			$arFields['~LOCATION'] = $arFields['LOCATION'];
			$arFields['LOCATION'] = CCalendar::GetTextLocation($arFields["LOCATION"]);
		}

		if (isset($arFields['~MEETING']))
		{
			$arFields['MEETING'] = $arFields['~MEETING'];
		}

		if (!empty($arFields['REMIND']) && !is_array($arFields['REMIND']))
		{
			$arFields['REMIND'] = unserialize($arFields['REMIND'], ['allowed_classes' => false]);
		}
		if (!is_array($arFields['REMIND'] ?? null))
		{
			$arFields['REMIND'] = [];
		}

		$arFields['RRULE'] = self::ParseRRULE($arFields['RRULE']);

		return $arFields;
	}

	public static function UpdateUserFields($eventId, $arFields = [], $updateSearchIndex = true)
	{
		$eventId = (int)$eventId;
		if (!is_array($arFields) || empty($arFields) || $eventId <= 0)
		{
			return false;
		}

		global $USER_FIELD_MANAGER;
		if ($USER_FIELD_MANAGER->CheckFields("CALENDAR_EVENT", $eventId, $arFields))
		{
			$USER_FIELD_MANAGER->Update("CALENDAR_EVENT", $eventId, $arFields);
		}

		foreach(GetModuleEvents("calendar", "OnAfterCalendarEventUserFieldsUpdate", true) as $arEvent)
		{
			ExecuteModuleEventEx($arEvent, array($eventId, $arFields));
		}

		if ($updateSearchIndex)
		{
			self::updateSearchIndex($eventId);
		}

		return true;
	}

	public static function Delete($params)
	{
		global $CACHE_MANAGER;
		$bCalDav = CCalendar::IsCalDAVEnabled();
		$id = (int)$params['id'];
		$sendNotification = ($params['sendNotification'] ?? null) !== false;
		$params['requestUid'] = $params['requestUid'] ?? null;

		if ($id)
		{
			$userId = (isset($params['userId']) && (int)$params['userId'] > 0)
				? (int)$params['userId']
				: CCalendar::GetCurUserId()
			;

			$arAffectedSections = [];
			$entry = $params['Event'] ?? null;

			if (!isset($entry) || !is_array($entry))
			{
				CCalendar::SetOffset();
				$res = self::GetList([
					'arFilter' => [
						'ID' => $id,
					],
					'parseRecursion' => false,
				]);
				$entry = $res[0] ?? null;
			}

			if ($entry)
			{
				$entry['PARENT_ID'] = $entry['PARENT_ID'] ?? null;
				if (!empty($entry['IS_MEETING']) && $entry['PARENT_ID'] !== $entry['ID'])
				{
					$parentEvent = self::GetList([
						'arFilter' => [
							"ID" => $entry['PARENT_ID'],
						],
						'parseRecursion' => false,
                    ]);
					$parentEvent = $parentEvent[0];
					if ($parentEvent)
					{
						$accessController = new EventAccessController($userId);
						$eventModel = self::getEventModelForPermissionCheck(
							(int)($entry['ID'] ?? 0),
							$entry,
							$userId
						);

						$perm = $accessController->check(ActionDictionary::ACTION_EVENT_DELETE, $eventModel);
						if (!$perm)
						{
							if (in_array($entry['MEETING_STATUS'] ?? null,
								[
									Dictionary::MEETING_STATUS['Yes'],
									Dictionary::MEETING_STATUS['Question'],
								], true)
							)
							{
								self::SetMeetingStatus([
									'userId' => $userId,
									'eventId' => $entry['ID'],
									'status' => 'N',
									'doSendMail' => false,
								]);
							}

							return true;
						}

						return CCalendar::DeleteEvent($parentEvent['ID']);
					}

					return false;
				}
				foreach(GetModuleEvents("calendar", "OnBeforeCalendarEventDelete", true) as $arEvent)
				{
					ExecuteModuleEventEx($arEvent, array($id, $entry));
				}

				if (!empty($entry['PARENT_ID']))
				{
					CCalendarLiveFeed::OnDeleteCalendarEventEntry($entry['PARENT_ID']);
				}
				else
				{
					CCalendarLiveFeed::OnDeleteCalendarEventEntry($entry['ID']);
				}

				$sharingOwnerId = -1;
				$eventLink = null;
				$eventType = $entry['EVENT_TYPE'] ?? '';

				if (in_array($eventType, Sharing\SharingEventManager::getSharingEventTypes(), true))
				{
					$eventId = (int)($entry['ID'] ?? 0);
					$initiatorId = CCalendar::GetCurUserId() !== 0
						? CCalendar::GetCurUserId()
						: $userId
					;

					Sharing\SharingEventManager::onSharingEventDeleted(
						$eventId,
						$eventType,
						$initiatorId,
					);

					$linkFactory = (new Sharing\Link\Factory());

					/** @var Sharing\Link\EventLink $eventLink */
					$eventLink = $linkFactory->getEventLinkByEventId((int)($entry['PARENT_ID'] ?? $eventId));
					if ($eventLink)
					{
						$sharingOwnerId = $eventLink->getOwnerId();
					}
				}

				$arAffectedSections[] = $entry['SECT_ID'];
				// Check location: if reserve meeting was reserved - clean reservation
				if (!empty($entry['LOCATION']))
				{
					$loc = Rooms\Util::parseLocation($entry['LOCATION']);
					if ($loc['mrevid'] || $loc['room_event_id'])
					{
						Rooms\Util::releaseLocation($loc);
					}
				}

				if ($entry['CAL_TYPE'] === 'user')
				{
					$CACHE_MANAGER->ClearByTag('calendar_user_'.$entry['OWNER_ID']);
				}

				if (!empty($entry['IS_MEETING']))
				{
					$isPastEvent = (int)$entry['DATE_TO_TS_UTC'] < (time() - (int)$entry['TZ_OFFSET_TO']);;
					CCalendarNotify::ClearNotifications($entry['PARENT_ID']);

					if (Loader::includeModule("im"))
					{
						CIMNotify::DeleteBySubTag("CALENDAR|INVITE|".$entry['PARENT_ID']);
						CIMNotify::DeleteBySubTag("CALENDAR|STATUS|".$entry['PARENT_ID']);
					}

					$involvedAttendees = [];

					$CACHE_MANAGER->ClearByTag('calendar_user_'.$userId);
					$childEvents = self::GetList([
						'arFilter' => [
							"PARENT_ID" => $id,
						],
						'parseRecursion' => false,
						'checkPermissions' => false,
						'setDefaultLimit' => false,
					]);

					foreach($childEvents as $chEvent)
					{
						$CACHE_MANAGER->ClearByTag('calendar_user_'.$chEvent["OWNER_ID"]);
						if (
							$chEvent["MEETING_STATUS"] !== "N"
							&& $sendNotification
							&& !$isPastEvent
							&& $sharingOwnerId !== (int)$chEvent["OWNER_ID"]
						)
						{
							$fromTo = self::GetEventFromToForUser($entry, $chEvent["OWNER_ID"]);
							$sendCancelUserId = $userId;
							if ($userId === 0 && ($eventLink instanceof Sharing\Link\EventLink))
							{
								$sendCancelUserId = $eventLink->getHostId();
							}
							if (
								(!empty($chEvent['MEETING_HOST']) && (int)$chEvent['MEETING_HOST'] === $sendCancelUserId)
								|| self::checkAttendeeBelongsToEvent($id, $sendCancelUserId)
							)
							{
								if (($eventLink instanceof Sharing\Link\EventLink))
								{
									\CCalendarNotify::Send([
										'mode' => 'cancel_sharing',
										'userId' => $sendCancelUserId,
										'guestId' => $chEvent['OWNER_ID'],
										'eventId' => $id,
										'name' => $chEvent['NAME'],
										'from' => $fromTo['DATE_FROM'],
										'to' => $fromTo['DATE_TO'],
										'isSharing' => true,
									]);
								}
								else
								{
									// first problematic place
									\CCalendarNotify::Send([
										'mode' => 'cancel',
										'name' => $chEvent['NAME'],
										"from" => $fromTo["DATE_FROM"],
										"to" => $fromTo["DATE_TO"],
										"location" => CCalendar::GetTextLocation($chEvent["LOCATION"]),
										"guestId" => $chEvent["OWNER_ID"],
										"eventId" => $id,
										"userId" => $sendCancelUserId,
									]);
								}
							}
						}

						if ($chEvent["MEETING_STATUS"] === "Q")
						{
							$involvedAttendees[] = $chEvent["OWNER_ID"];
						}

						$bExchange = CCalendar::IsExchangeEnabled($chEvent["OWNER_ID"]);
						if ($bExchange || $bCalDav)
						{
							CCalendarSync::DoDeleteToDav(
								[
									'bCalDav' => $bCalDav,
									'bExchangeEnabled' => $bExchange,
									'sectionId' => $chEvent['SECT_ID'],
								],
								$chEvent
							);
						}

						self::onEventDelete($chEvent, $params);

						$isParent = $chEvent['ID'] === $chEvent['PARENT_ID'];
						if (
							!$isParent
							&& !$isPastEvent
							&& ICalUtil::isMailUser($chEvent['OWNER_ID'])
						)
						{
							if (!empty($chEvent['ATTENDEE_LIST']) && is_array($chEvent['ATTENDEE_LIST']))
							{
								$attendeeIds = [];
								foreach ($chEvent['ATTENDEE_LIST'] as $attendee)
								{
									$attendeeIds[] = $attendee['id'];
								}
							}
							$attendees = null;
							if (!empty($attendeeIds))
							{
								$attendees = ICalUtil::getIndexUsersById($attendeeIds);
							}

							$sender = self::getSenderForIcal($attendees, $chEvent['MEETING_HOST']);
							if (!empty($chEvent['MEETING']['MAIL_FROM']))
							{
								$sender['EMAIL'] = $chEvent['MEETING']['MAIL_FROM'];
								$sender['MAIL_FROM'] = $chEvent['MEETING']['MAIL_FROM'];
							}
							else
							{
								continue;
							}

							/** increment version to delete event in outside service */
							$chEvent['VERSION'] = (int)$chEvent['VERSION'] + 1;

							$invitationInfo = (new InvitationInfo(
								(int)$chEvent['ID'],
								(int)$sender['ID'],
								(int)$chEvent['OWNER_ID'],
								InvitationInfo::TYPE_CANCEL
							))->toArray();

							SendingEmailNotification::sendMessageToQueue($invitationInfo);
						}

						$pullUserId = (int)$chEvent['OWNER_ID'] > 0 ? (int)$chEvent['OWNER_ID'] : $userId;
						if (
							$pullUserId
							&& self::$sendPush
						)
						{
							Util::addPullEvent(
								PushCommand::DeleteEvent,
								$pullUserId,
								[
									'fields' => $chEvent,
									'requestUid' => $params['requestUid'] ?? null,
								]
							);
						}
					}

					// Set flag
					if (!empty($params['bMarkDeleted']))
					{
						Internals\EventTable::updateByFilter(
							['=PARENT_ID' => $id],
							['DELETED' => 'Y'],
						);
					}
					else // Actual deleting
					{
						Internals\EventTable::deleteByFilter(['PARENT_ID' => $id]);
					}

					if (!empty($involvedAttendees))
					{
						CCalendar::UpdateCounter($involvedAttendees, [$id]);
					}
				}

				if (!$entry['IS_MEETING'] && $entry['CAL_TYPE'] === 'user')
				{
					self::onEventDelete($entry, $params);
				}

				if (
					$params
					&& is_array($params)
					&& \Bitrix\Calendar\Sync\Util\RequestLogger::isEnabled()
				)
				{
					$loggerData = $params;
					unset($loggerData['Event']);
					$loggerData['loggerUuid'] = $id;
					(new \Bitrix\Calendar\Sync\Util\RequestLogger($userId, 'portal_delete'))->write($loggerData);
				}

				if (!empty($params['bMarkDeleted']))
				{
					Internals\EventTable::update($id, ['DELETED' => 'Y']);
				}
				else
				{
					// Real deleting
					Internals\EventTable::delete($id);
				}

				if (!empty($arAffectedSections))
				{
					CCalendarSect::UpdateModificationLabel($arAffectedSections);
				}

				foreach(EventManager::getInstance()->findEventHandlers("calendar", "OnAfterCalendarEventDelete") as $event)
				{
					ExecuteModuleEventEx($event, [$id, $entry, $userId]);
				}

				CCalendar::ClearCache('event_list');

				if (($entry['ACCESSIBILITY'] ?? '') === 'absent')
				{
					(new \Bitrix\Calendar\Integration\Intranet\Absence())->cleanCache();
				}

				$pullUserId = (int)$entry['OWNER_ID'] > 0 ? (int)$entry['OWNER_ID'] : $userId;
				if (
					$pullUserId
					&& self::$sendPush
				)
				{
					Util::addPullEvent(
						PushCommand::DeleteEvent,
						$pullUserId,
						[
							'fields' => $entry,
							'requestUid' => $params['requestUid'] ?? null,
						]
					);
				}

				(new \Bitrix\Calendar\Event\Event\AfterCalendarEventDeleted($id))->emit();

				return true;
			}
		}

		return false;
	}

	public static function SetMeetingStatusEx($params)
	{
		$doSendMail = $params['doSendMail'] ?? true;
		$reccurentMode = isset($params['reccurentMode'])
			&& in_array($params['reccurentMode'], ['this', 'next', 'all'])
				? $params['reccurentMode']
				: false;

		$currentDateFrom = CCalendar::Date(CCalendar::Timestamp($params['currentDateFrom']), false);
		if ($reccurentMode && $currentDateFrom)
		{
			$event = self::GetById($params['parentId'], false);
			$recurrenceId = $event['RECURRENCE_ID'] ?? $event['ID'];

			if ($reccurentMode !== 'all')
			{
				$res = CCalendar::SaveEventEx([
					'arFields' => [
						'ID' => $params['parentId'],
					],
					'silentErrorMode' => false,
					'recursionEditMode' => $reccurentMode,
					'userId' => $event['MEETING_HOST'],
					'checkPermission' => false,
					'currentEventDateFrom' => $currentDateFrom,
					'sendEditNotification' => false,
				]);

				if (
					$res
					&& isset($res['recEventId'])
					&& $res['recEventId']
				)
				{
					self::SetMeetingStatus([
						'userId' => $params['attendeeId'],
						'eventId' => $res['recEventId'],
						'status' => $params['status'],
						'personalNotification' => true,
						'doSendMail' => $doSendMail,
					]);
				}
			}

			if ($reccurentMode === 'all' || $reccurentMode === 'next')
			{
				$recRelatedEvents = self::GetEventsByRecId($recurrenceId, false);

				if ($reccurentMode === 'next')
				{
					$untilTimestamp = CCalendar::Timestamp($currentDateFrom);
				}
				else
				{
					$untilTimestamp = false;
					self::SetMeetingStatus([
						'userId' => $params['attendeeId'],
						'eventId' => $params['eventId'],
						'status' => $params['status'],
						'personalNotification' => true,
						'doSendMail' => $doSendMail,
					]);
				}

				foreach($recRelatedEvents as $ev)
				{
					if ((int)$ev['ID'] === (int)($params['eventId'] ?? 0))
					{
						continue;
					}

					if ($reccurentMode === 'all'
						|| (
							$untilTimestamp
							&& CCalendar::Timestamp($ev['DATE_FROM']) > $untilTimestamp
						)
					)
					{
						self::SetMeetingStatus([
							'userId' => $params['attendeeId'],
							'eventId' => $ev['ID'],
							'status' => $params['status'],
							'doSendMail' => $doSendMail,
						]);
					}
				}
			}
		}
		else
		{
			self::SetMeetingStatus([
				'userId' => $params['attendeeId'] ?? null,
				'eventId' => $params['eventId'] ?? null,
				'status' => $params['status'] ?? null,
				'doSendMail' => $doSendMail,
			]);
		}
	}

	public static function SetMeetingStatus($params)
	{
		$eventId = $params['eventId'] = (int)($params['eventId'] ?? 0);
		if (!$eventId)
		{
			return;
		}

		CTimeZone::Disable();
		global $CACHE_MANAGER;
		$userId = $params['userId'] = (int)$params['userId'];
		$status = mb_strtoupper($params['status']);
		$doSendMail = $params['doSendMail'] ?? true;
		$prevStatus = null;
		if (!in_array($status, ["Q", "Y", "N", "H", "M"], true))
		{
			$status = $params['status'] = "Q";
		}

		$event = self::GetList([
			'arFilter' => [
				"ID" => $eventId,
				"IS_MEETING" => 1,
				"DELETED" => "N",
			],
			'parseRecursion' => false,
			'fetchAttendees' => true,
			'fetchMeetings' => true,
			'checkPermissions' => false,
			'setDefaultLimit' => false,
		]);

		if (!empty($event))
		{
			$event = $event[0];
			$prevStatus = $event['MEETING_STATUS'];
		}

		if ($event && $event['IS_MEETING'] && (int)$event['PARENT_ID'] > 0)
		{
			if (in_array($event['EVENT_TYPE'], Sharing\SharingEventManager::getSharingEventTypes(), true))
			{
				$userEventForSharing = self::GetList([
					'arFilter' => [
						'PARENT_ID' => $event['PARENT_ID'],
						'OWNER_ID' => $userId,
						'IS_MEETING' => 1,
						'DELETED' => 'N',
					],
					'checkPermissions' => false,
				]);

				if (!empty($userEventForSharing))
				{
					$userEventForSharing = $userEventForSharing[0];
				}
			}

			if (ICalUtil::isMailUser($event['MEETING_HOST']))
			{
				if (\Bitrix\Main\Config\Option::get('calendar', 'log_mail_send_meeting_status', 'N') === 'Y')
				{
					(new Internals\Log\Logger('DEBUG_CALENDAR_MAIL_SEND_MEETING_STATUS'))
						->log(['eventId' => $event['ID'], 'userId' => $userId, 'status' => $status], 10)
					;
				}
				if ($doSendMail && $prevStatus !== $status)
				{
					IncomingEventManager::rehandleRequest([
						'event' => $event,
						'userId' => $userId,
						'answer' => $status === 'Y',
					]);
				}
			}

			if ($event['CURRENT_ATTENDEE_ID'] ?? null)
			{
				self::updateEventAttendeeMeetingStatus((int)$event['CURRENT_ATTENDEE_ID'], $status);
			}

			Internals\EventTable::updateByFilter(
				[
					'=PARENT_ID' => (int)$event['PARENT_ID'],
					'=OWNER_ID' => $userId,
					'=CAL_TYPE' => Dictionary::CALENDAR_TYPE['user'],
				],
				['MEETING_STATUS' => $status],
			);

			if ($status === 'Y')
			{
				self::ShowEventSection((int)$event['PARENT_ID'], $userId);
			}

			CCalendarSect::UpdateModificationLabel($event['SECT_ID']);

			// Clear invitation in messenger
			CCalendarNotify::ClearNotifications($event['PARENT_ID'], $userId);

			// Add new notification in messenger
			if (!empty($params['personalNotification']) && CCalendar::getCurUserId() === $userId)
			{
				$fromTo = self::GetEventFromToForUser($event, $userId);
				CCalendarNotify::Send([
					'mode' => $status === "Y" ? 'status_accept' : 'status_decline',
					'name' => $event['NAME'],
					"from" => $fromTo["DATE_FROM"],
					"guestId" => $userId,
					"eventId" => $event['PARENT_ID'],
					"userId" => $userId,
					"markRead" => true,
					"fields" => $event,
				]);
			}

			$shouldNotifyExtranetInCollab = $status === 'Y'
				&& $event['EVENT_TYPE'] === Dictionary::EVENT_TYPE['collab']
				&& Util::isCollabUser($userId)
			;

			if ($shouldNotifyExtranetInCollab)
			{
				$fromTo = self::GetEventFromToForUser($event, $userId);
				CCalendarNotify::Send([
					'mode' => 'ics_link',
					'name' => $event['NAME'],
					'from' => $fromTo['DATE_FROM'],
					'guestId' => $userId,
					'eventId' => $event['PARENT_ID'],
					'userId' => $userId,
					'markRead' => true,
					'fields' => $event,
				]);
			}

			if (
				$status === 'Y'
				&& ($params['sharingAutoAccept'] ?? null) === true
				&& in_array($event['EVENT_TYPE'], Sharing\SharingEventManager::getSharingEventTypes(), true)
			)
			{
				$fromTo = self::GetEventFromToForUser($event, $userId);
				CCalendarNotify::Send([
					'mode' => 'status_accept',
					'name' => $event['NAME'],
					"from" => $fromTo["DATE_FROM"],
					"guestId" => (int)($event['MEETING_HOST'] ?? null),
					"eventId" => $event['PARENT_ID'],
					"userId" => $userId,
					"fields" => $event,
					'isSharing' => true,
				]);
			}

			$addedPullUserList = [];
			if (isset($event['ATTENDEE_LIST']) && is_array($event['ATTENDEE_LIST']))
			{
				foreach ($event['ATTENDEE_LIST'] as $attendee)
				{
					$fields = [
						'MEETING_STATUS' => $status,
						'USER_ID' => $userId,
						'PARENT_ID' => $event['PARENT_ID'] ?? $eventId,
						'ATTENDEES' => $event['ATTENDEES'] ?? null,
						'CAL_TYPE' => $event['CAL_TYPE'] ?? null,
					];

					Util::addPullEvent(
						PushCommand::SetMeetingStatus,
						$attendee['id'],
						[
							'fields' => $fields,
							'requestUid' => $params['requestUid'] ?? null,
						]
					);
					$addedPullUserList[] = (int)$attendee['id'];
				}
			}

			$pullUserId = (int)($event['OWNER_ID'] ?? $userId);
			if ($pullUserId && !in_array($pullUserId, $addedPullUserList, true))
			{
				$fields = [
					'MEETING_STATUS' => $status,
					'USER_ID' => $userId,
					'PARENT_ID' => $event['PARENT_ID'] ?? $eventId,
					'ATTENDEES' => $event['ATTENDEES'] ?? null,
					'CAL_TYPE' => $event['CAL_TYPE'] ?? null,
				];

				Util::addPullEvent(
					PushCommand::SetMeetingStatus,
					$pullUserId,
					[
						'fields' => $fields,
						'requestUid' => $params['requestUid'] ?? null,
					]
				);
			}

			// Notify author of event
			if (
				$event['MEETING']['NOTIFY']
				&& (int)$event['MEETING_HOST']
				&& $userId !== (int)$event['MEETING_HOST']
				&& ($params['hostNotification'] ?? null) !== false
			)
			{
				if (self::checkAttendeeBelongsToEvent($event['PARENT_ID'], $userId))
				{
					// Send message to the author
					$fromTo = self::GetEventFromToForUser($event, $event['MEETING_HOST']);
					CCalendarNotify::Send([
						'mode' => $status === "Y" ? 'accept' : 'decline',
						'name' => $event['NAME'],
						"from" => $fromTo["DATE_FROM"],
						"to" => $fromTo["DATE_TO"],
						"location" => CCalendar::GetTextLocation($event["LOCATION"] ?? null),
						"guestId" => $userId,
						"eventId" => $event['PARENT_ID'],
						"userId" => $event['MEETING']['MEETING_CREATOR'] ?? $event['MEETING_HOST'],
						"fields" => $event,
					]);
				}

				if (
					!empty($userEventForSharing)
					&& in_array($event['EVENT_TYPE'], Sharing\SharingEventManager::getSharingEventTypes(), true)
				)
				{
					Sharing\SharingEventManager::onSharingEventMeetingStatusChange(
						$userId,
						$status,
						$userEventForSharing,
						$params['sharingAutoAccept'] ?? false
					);
				}

			}
			CCalendarSect::UpdateModificationLabel([$event['SECTIONS'][0] ?? null]);

			if ($status === "N")
			{
				$childEvent = self::GetList([
					'arFilter' => [
						"PARENT_ID" => $event['PARENT_ID'],
						"CREATED_BY" => $userId,
						"IS_MEETING" => 1,
						"DELETED" => "N",
					],
					'parseRecursion' => false,
					'fetchAttendees' => true,
					'checkPermissions' => false,
					'setDefaultLimit' => false,
				]);

				if ($childEvent && $childEvent[0])
				{
					$childEvent = $childEvent[0];
					$bCalDav = CCalendar::IsCalDAVEnabled();
					$bExchange = CCalendar::IsExchangeEnabled($userId);

					if ($bExchange || $bCalDav)
					{
						CCalendarSync::DoDeleteToDav([
							'bCalDav' => $bCalDav,
							'bExchangeEnabled' => $bExchange,
							'sectionId' => $childEvent['SECT_ID'],
						], $childEvent);
					}

					self::onEventDelete($childEvent);
				}
			}

			if ($status === "Y")
			{
				if (($params['affectRecRelatedEvents'] ?? null) !== false)
				{
					$event = self::GetList([
						'arFilter' => [
							"ID" => $eventId,
							"IS_MEETING" => 1,
							"DELETED" => "N",
						],
						'parseRecursion' => false,
						'fetchAttendees' => true,
						'fetchMeetings' => true,
						'checkPermissions' => false,
						'setDefaultLimit' => false,
					]);

					if (!empty($event))
					{
						$event = $event[0];
					}

					if (!empty($event['RECURRENCE_ID']))
					{
						$masterEvent = self::GetList([
							'arFilter' => [
								'PARENT_ID' => $event['RECURRENCE_ID'],
								'DELETED' => 'N',
								'OWNER_ID' => $userId,
							],
							'parseRecursion' => false,
							'fetchAttendees' => false,
							'checkPermissions' => false,
							'setDefaultLimit' => false,
						]);
						if (!empty($masterEvent))
						{
							$masterEvent = $masterEvent[0];
						}

						if (($masterEvent['MEETING_STATUS'] ?? null) !== 'Y')
						{
							self::SetMeetingStatus([
								'userId' => $userId,
								'eventId' => $masterEvent['ID'],
								'status' => $status,
								'personalNotification' => true,
								'hostNotification' => true,
								'affectRecRelatedEvents' => false,
								'updateDescription' => $params['updateDescription'] ?? null,
							]);

							self::SetMeetingStatusForRecurrenceEvents(
								$event['RECURRENCE_ID'],
								$userId,
								$params['eventId'],
								$status,
								$params['updateDescription'] ?? null,
							);
						}
					}

					if (!empty($event['RRULE']) && in_array($prevStatus, ['N', 'Q', 'H']))
					{
						self::SetMeetingStatusForRecurrenceEvents(
							$event['PARENT_ID'],
							$userId,
							$params['eventId'],
							$status,
							$params['updateDescription'] ?? null,
						);
					}
				}

				if ($prevStatus === 'N')
				{
					CCalendar::syncChange(
						$eventId,
						[
							"MEETING_STATUS" => $status,
						],
						[
							'userId' => $userId,
							'originalFrom' => null,
						],
						null //$event
					);
				}
			}

			if (($params['updateDescription'] ?? null) !== false)
			{
				if (!empty($event['RECURRENCE_ID']))
				{
					self::pushUpdateDescriptionToQueue($event['RECURRENCE_ID'], $userId, $status);
				}
				if (!empty($event['PARENT_ID']) && (int)$event['PARENT_ID'] !== (int)$event['RECURRENCE_ID'])
				{
					self::pushUpdateDescriptionToQueue($event['PARENT_ID'], $userId, $status);
				}
			}

			CCalendarLiveFeed::OnChangeMeetingStatusEventEntry([
				'userId' => $userId,
				'eventId' => $eventId,
				'status' => $status,
				'event' => $event,
			]);

			CCalendar::UpdateCounter([$userId], [$eventId]);

			$CACHE_MANAGER->ClearByTag('calendar_user_' . $userId);

			$createdBy = (int)$event['CREATED_BY'];
			if ($createdBy !== $userId)
			{
				$CACHE_MANAGER->ClearByTag('calendar_user_' . $createdBy);
			}

			if (($event['ACCESSIBILITY'] ?? '') === 'absent')
			{
				(new \Bitrix\Calendar\Integration\Intranet\Absence())->cleanCache();
			}
		}
		else
		{
			CCalendarNotify::ClearNotifications($eventId);
		}

		CTimeZone::Enable();
		CCalendar::ClearCache(['attendees_list', 'event_list']);

		(new \Bitrix\Calendar\Event\Event\AfterMeetingStatusChanged($eventId, $userId, $status))->emit();
	}

	private static function updateEventAttendeeMeetingStatus(int $eventAttendeeId, string $meetingStatus): void
	{
		$updateResult = Internals\EventAttendeeTable::update(
			$eventAttendeeId,
			[
				'MEETING_STATUS' => $meetingStatus,
			]
		);
		if ($updateResult->isSuccess())
		{
		}
	}

	private static function updateEventAttendeeColor(int $eventAttendeeId, string $color): void
	{
		$updateResult = Internals\EventAttendeeTable::update(
			$eventAttendeeId,
			[
				'COLOR' => $color,
			]
		);
		if ($updateResult->isSuccess())
		{
		}
	}

	private static function getEventAttendeeIdByEventIdAndUserId(int $eventId, int $userId): ?int
	{
		$eventAttendee = Internals\EventAttendeeTable::getRow([
			'select' => ['ID'],
			'filter' => [
				'EVENT_ID' => $eventId,
				'OWNER_ID' => $userId,
			],
		]);

		return $eventAttendee ? (int)$eventAttendee['ID'] : null;
	}

	private static function safeDeleteEventAttendees(int $eventId, array $attendeeUserIds): void
	{
		$eventAttendeesResult = Internals\EventAttendeeTable::getList([
			'select' => ['ID'],
			'filter' => [
				'EVENT_ID' => $eventId,
				'OWNER_ID' => $attendeeUserIds,
			],
		]);

		$eventAttendees = $eventAttendeesResult->fetchAll();
		if (empty($eventAttendees))
		{
			return;
		}

		$eventAttendeeIds = array_column($eventAttendees, 'ID');

		$updateResult = Internals\EventAttendeeTable::updateMulti($eventAttendeeIds, [
			'DELETED' => 'Y',
		]);

		if ($updateResult->isSuccess())
		{
		}
	}

	private static function updateEventAttendee(
		int $parentEventId,
		int $userId,
		array $changedFields,
		array $dbFields
	): void
	{
		if (in_array('COLOR', $changedFields, true) && isset($dbFields['COLOR']))
		{
			// color changed in all child events
			$allChildEventAttendees = Internals\EventAttendeeTable::getList([
				'select' => ['ID'],
				'filter' => [
					'EVENT_ID' => $parentEventId,
				],
			]);
			$eventAttendeeIds = array_column($allChildEventAttendees->fetchAll(), 'ID');
			// for event not stored in b_calendar_event_attendees
			if (!$eventAttendeeIds)
			{
				return;
			}
			$colorUpdateResult = Internals\EventAttendeeTable::updateMulti(
				$eventAttendeeIds,
				[
					'COLOR' => $dbFields['COLOR'],
				],
			);
			if ($colorUpdateResult->isSuccess())
			{
			}
		}

		// remind changed only for given user
		if (in_array('REMIND', $changedFields, true) && isset($dbFields['REMIND']))
		{
			$eventAttendeeId = self::getEventAttendeeIdByEventIdAndUserId($parentEventId, $userId);
			// for event not stored in b_calendar_event_attendees
			if (!$eventAttendeeId)
			{
				return;
			}

			$remindUpdateResult = Internals\EventAttendeeTable::update(
				$eventAttendeeId,
				[
					'REMIND' => $dbFields['REMIND'],
				],
			);
			if ($remindUpdateResult->isSuccess())
			{
			}
		}
	}

	protected static function ShowEventSection(int $parentId, int $userId): void
	{
		$eventEO = Internals\EventTable::query()
			->setSelect(['*'])
			->where('PARENT_ID', $parentId)
			->where('OWNER_ID', $userId)
			->exec()->fetchObject();

		if (is_null($eventEO))
		{
			return;
		}

		/** @var Event $acceptedEvent */
		$acceptedEvent = (new Mappers\Event())->getByEntityObject($eventEO);

		if (is_null($acceptedEvent))
		{
			return;
		}

		$acceptedSectionId = $acceptedEvent->getSection()->getId();
		$hiddenSectionIds = UserSettings::getHiddenSections($userId, [ 'isPersonalCalendarContext' => true ]);
		$newHiddenSectionIds = array_filter($hiddenSectionIds, static function($hiddenSectionId) use ($acceptedSectionId) {
			return !is_numeric($hiddenSectionId) || (int)$hiddenSectionId !== $acceptedSectionId;
		});

		if (count($hiddenSectionIds) === count($newHiddenSectionIds))
		{
			return;
		}

		UserSettings::saveHiddenSections($userId, $newHiddenSectionIds);
		Util::addPullEvent(
			PushCommand::HiddenSectionsUpdated,
			$userId,
			[
				'hiddenSections' => $newHiddenSectionIds,
			]
		);
	}

	public static function SetMeetingStatusForRecurrenceEvents(
		int $recurrenceId,
		int $userId,
		int $eventId,
		string $status,
		?bool $updateDescription = null,
	): void
	{
		$recRelatedEvents = self::GetEventsByRecId($recurrenceId, false, $userId);
		foreach ($recRelatedEvents as $ev)
		{
			if ((int)$ev['ID'] === $eventId)
			{
				continue;
			}

			self::SetMeetingStatus([
				'userId' => $userId,
				'eventId' => $ev['ID'],
				'status' => $status,
				'personalNotification' => false,
				'hostNotification' => false,
				'affectRecRelatedEvents' => false,
				'updateDescription' => $updateDescription,
			]);
		}
	}

	/*
	 * $params['dateFrom']
	 * $params['dateTo']
	 *
	 * */

	public static function GetMeetingStatus($userId, $eventId)
	{
		$eventId = (int)$eventId;
		$userId = (int)$userId;
		$status = false;
		$event = self::GetById($eventId, false);
		if ($event && $event['IS_MEETING'] && (int)$event['PARENT_ID'] > 0)
		{
			if ((int)$event['OWNER_ID'] !== $userId)
			{
				$event = Internals\EventTable::query()
					->setSelect(['MEETING_STATUS'])
					->where('PARENT_ID', (int)$event['PARENT_ID'])
					->where('OWNER_ID', $userId)
					->setLimit(1)
					->exec()->fetch()
				;
			}

			$status = $event['MEETING_STATUS'];
		}
		return $status;
	}

	/**
	 * @param array $params
	 * $params = [
	 *      'curEventId' => (int)
	 *      'userId' => (int)
	 *      'checkPermissions' => (bool)
	 * 		users => int[]
	 * 		from => string Time start period
	 * 		to => string Time finish period
	 *    ]
	 *
	 *
	 * @return array
	 */
	public static function GetAccessibilityForUsers($params = []): array
	{
		$curEventId = (int)$params['curEventId'];
		$curUserId = isset($params['userId']) ? (int)$params['userId'] : CCalendar::GetCurUserId();
		if (!is_array($params['users']) || !count($params['users']))
		{
			return [];
		}

		if (!isset($params['checkPermissions']))
		{
			$params['checkPermissions'] = true;
		}

		$users = [];
		$accessibility = [];
		foreach($params['users'] as $userId)
		{
			$userId = (int)$userId;
			if ($userId)
			{
				$users[] = $userId;
				$accessibility[$userId] = [];
			}
		}

		if (empty($users))
		{
			return [];
		}

		$events = self::GetList([
			'arFilter' => [
				"FROM_LIMIT" => $params['from'],
				"TO_LIMIT" => $params['to'],
				"CAL_TYPE" => 'user',
				"OWNER_ID" => $users,
				"ACTIVE_SECTION" => "Y",
			],
			'arSelect' => self::$defaultSelectEvent,
			'parseRecursion' => true,
			'fetchAttendees' => false,
			'fetchSection' => true,
			'parseDescription' => false,
			'setDefaultLimit' => false,
			'checkPermissions' => $params['checkPermissions'],
		]);

		foreach ($events as $event)
		{
			if ($curEventId && ((int)$event["ID"] === $curEventId || (int)$event["PARENT_ID"] === $curEventId))
			{
				continue;
			}
			if ($event["ACCESSIBILITY"] === 'free')
			{
				continue;
			}
			if ($event["IS_MEETING"] && $event["MEETING_STATUS"] === "N")
			{
				continue;
			}
			if (CCalendarSect::CheckGoogleVirtualSection($event['SECTION_DAV_XML_ID']))
			{
				continue;
			}

			$name = $event["NAME"];
			$accessController = new EventAccessController($curUserId);
			$eventModel = EventModel::createFromArray($event);

			if (
				($event['PRIVATE_EVENT'] && $event['CAL_TYPE'] === 'user' && $event['OWNER_ID'] !== $curUserId)
				|| !$accessController->check(ActionDictionary::ACTION_EVENT_VIEW_TITLE, $eventModel)
			)
			{
				$name = '[' . Loc::getMessage('EC_ACCESSIBILITY_' . mb_strtoupper($event['ACCESSIBILITY'])) . ']';
			}

			$accessibility[$event['OWNER_ID']][] = [
				"ID" => $event["ID"],
				"NAME" => $name,
				"DATE_FROM" => $event["DATE_FROM"],
				"DATE_TO" => $event["DATE_TO"],
				"DATE_FROM_TS_UTC" => $event["DATE_FROM_TS_UTC"],
				"DATE_TO_TS_UTC" => $event["DATE_TO_TS_UTC"],
				"~USER_OFFSET_FROM" => $event["~USER_OFFSET_FROM"],
				"~USER_OFFSET_TO" => $event["~USER_OFFSET_TO"],
				"DT_SKIP_TIME" => $event["DT_SKIP_TIME"],
				"TZ_FROM" => $event["TZ_FROM"],
				"TZ_TO" => $event["TZ_TO"],
				"ACCESSIBILITY" => $event["ACCESSIBILITY"],
				"IMPORTANCE" => $event["IMPORTANCE"],
				"EVENT_TYPE" => $event["EVENT_TYPE"],
			];
		}

		// foreach ($users as $userId)
		// {
		// 	$userSettings = UserSettings::get($userId);
		// 	$enableLunchTime = $userSettings['enableLunchTime'] === 'Y';
		//
		// 	if (!$enableLunchTime)
		// 	{
		// 		continue;
		// 	}
		//
		// 	$lunchStart = CCalendar::Timestamp("{$params['from']} {$userSettings['lunchStart']}");
		// 	$lunchEnd = CCalendar::Timestamp("{$params['from']} {$userSettings['lunchEnd']}");
		// 	$lunchLength = $lunchEnd - $lunchStart;
		//
		// 	$from = $lunchStart;
		// 	$to = CCalendar::Timestamp("{$params['to']} {$userSettings['lunchStart']}");
		// 	while ($from <= $to)
		// 	{
		// 		$dateFrom = (new \DateTime())->setTimestamp($from);
		// 		$dateTo = (new \DateTime())->setTimestamp($from + $lunchLength);
		// 		$lunchStart = DateTime::createFromPhp($dateFrom);
		// 		$lunchEnd = DateTime::createFromPhp($dateTo);
		// 		$eventTsFromUTC = Sharing\Helper::getEventTimestampUTC($lunchStart);
		// 		$eventTsToUTC = Sharing\Helper::getEventTimestampUTC($lunchEnd);
		// 		$accessibility[$userId][] = array(
		// 			'ID' => -1,
		// 			'NAME' => 'Lunch',
		// 			'DATE_FROM' => $lunchStart,
		// 			'DATE_TO' => $lunchEnd,
		// 			'TZ_FROM' => $dateFrom->getTimezone()->getName(),
		// 			'TZ_TO' => $dateTo->getTimezone()->getName(),
		// 			'DATE_FROM_TS_UTC' => $eventTsFromUTC,
		// 			'DATE_TO_TS_UTC' => $eventTsToUTC,
		// 			'~USER_OFFSET_FROM' => 0,
		// 			'~USER_OFFSET_TO' => 0,
		// 			'ACCESSIBILITY' => 'busy',
		// 			'IMPORTANCE' => 'normal',
		// 			'DT_SKIP_TIME' => false,
		// 		);
		//
		// 		$from += \CCalendar::DAY_LENGTH;
		// 	}
		// }

		return $accessibility;
	}

	public static function GetAbsent($users = null, $params = []): array
	{
		// Can be called from agent... So we have to create $USER if it is not exists
		$tempUser = CCalendar::TempUser(false, true);
		$checkPermissions = $params['checkPermissions'] !== false;
		$curUserId = isset($params['userId']) ? (int)$params['userId'] : CCalendar::GetCurUserId();
		$arUsers = [];

		if (is_array($users) && !empty($users))
		{
			foreach($users as $id)
			{
				if ($id > 0)
				{
					$arUsers[] = (int)$id;
				}
			}
			if (empty($arUsers))
			{
				$users = false;
			}
		}

		$arFilter = [
			'DELETED' => 'N',
			'ACCESSIBILITY' => 'absent',
			'CAL_TYPE' => Dictionary::CALENDAR_TYPE['user'],
		];

		if ($users)
		{
			$arFilter['CREATED_BY'] = $users;
		}

		if (isset($params['fromLimit']))
		{
			$arFilter['FROM_LIMIT'] = CCalendar::Date(CCalendar::Timestamp($params['fromLimit'], false), true, false);
		}

		if (isset($params['toLimit']))
		{
			$arFilter['TO_LIMIT'] = CCalendar::Date(CCalendar::Timestamp($params['toLimit'], false), true, false);
		}

		$eventList = self::GetList([
			'arFilter' => $arFilter,
			'arSelect' => self::$defaultSelectEvent,
			'parseRecursion' => true,
			'getUserfields' => false,
			'fetchAttendees' => false,
			'userId' => $curUserId,
			'preciseLimits' => true,
			'checkPermissions' => false,
			'parseDescription' => false,
			'skipDeclined' => true,
		]);

		$result = [];
		$settings = false;

		foreach($eventList as $event)
		{
			$userId = (int)$event['CREATED_BY'];
			if (!empty($users) && !in_array($userId, $arUsers, true))
			{
				continue;
			}

			if ($event['IS_MEETING'] && $event["MEETING_STATUS"] === 'N')
			{
				continue;
			}

			if (
				$checkPermissions
				&& ($event['CAL_TYPE'] !== 'user' || $curUserId !== (int)$event['OWNER_ID'])
				&& $curUserId !== (int)$event['CREATED_BY']
			)
			{
				$sectId = (int)$event['SECTION_ID'];
				if (empty($event['ACCESSIBILITY']))
				{
					$event['ACCESSIBILITY'] = 'busy';
				}

				if ($settings === false)
				{
					$settings = CCalendar::GetSettings(array('request' => false));
				}
				$private = $event['PRIVATE_EVENT'] && $event['CAL_TYPE'] === 'user';

				$accessController = new EventAccessController($userId);
				$eventModel = EventModel::createFromArray($event);
				$eventModel->setSectionId((int)$sectId);

				if ($private || !$accessController->check(ActionDictionary::ACTION_EVENT_VIEW_FULL, $eventModel))
				{
					$event = self::ApplyAccessRestrictions($event, $userId);
				}
			}

			$skipTime = $event['DT_SKIP_TIME'] === 'Y';
			$fromTs = CCalendar::Timestamp($event['DATE_FROM'], false, !$skipTime);
			$toTs = CCalendar::Timestamp($event['DATE_TO'], false, !$skipTime);
			if ($event['DT_SKIP_TIME'] !== 'Y')
			{
				$fromTs -= $event['~USER_OFFSET_FROM'];
				$toTs -= $event['~USER_OFFSET_TO'];
			}

			$result[] = [
				'ID' => $event['ID'],
				'NAME' => $event['NAME'],
				'DATE_FROM' => CCalendar::Date($fromTs, !$skipTime, false),
				'DATE_TO' => CCalendar::Date($toTs, !$skipTime, false),
				'DT_FROM_TS' => $fromTs,
				'DT_TO_TS' => $toTs,
				'CREATED_BY' => $userId,
				'DETAIL_TEXT' => '',
				'USER_ID' => $userId,
			];
		}

		// Sort by DATE_FROM_TS_UTC
		usort($result, static function($a, $b){
			if ($a['DT_FROM_TS'] === $b['DT_FROM_TS'])
			{
				return 0;
			}
			return $a['DT_FROM_TS'] < $b['DT_FROM_TS'] ? -1 : 1;
		});

		CCalendar::TempUser($tempUser, false);

		return $result;
	}

	public static function DeleteEmpty(int $sectionId)
	{
		if (!$sectionId)
		{
			return;
		}

		$query = Internals\EventTable::query()
			->setSelect(['ID', 'LOCATION', 'SECTION_ID'])
			->where('SECTION_ID', $sectionId)
			->exec()
		;

		while ($event = $query->fetch())
		{
			$loc = $event['LOCATION'] ?? null;
			if ($loc && mb_strlen($loc) > 5 && mb_strpos($loc, 'ECMR_') === 0)
			{
				$loc = Rooms\Util::parseLocation($loc);
				if ($loc['mrid'] !== false && $loc['mrevid'] !== false) // Release MR
				{
					Rooms\Util::releaseLocation($loc);
				}
			}
			else if ($loc && mb_strlen($loc) > 9 && mb_strpos($loc, 'calendar_') === 0)
			{
				$loc = Rooms\Util::parseLocation($loc);
				if ($loc['room_id'] !== false && $loc['room_event_id'] !== false) // Release calendar_room
				{
					Rooms\Util::releaseLocation($loc);
				}
			}
			$itemIds[] = (int)$event['ID'];
		}

		// Clean from 'b_calendar_event'
		if (!empty($itemIds))
		{
			Internals\EventTable::deleteByFilter([
				'ID' => $itemIds,
			]);
		}

		CCalendar::ClearCache([
			'section_list',
			'event_list',
		]);
	}

	public static function CleanEventsWithDeadParents()
	{
		global $DB;
		$strSql = "SELECT PARENT_ID from b_calendar_event where PARENT_ID in (SELECT ID from b_calendar_event where MEETING_STATUS='H' and DELETED='Y') AND DELETED='N'";
		$res = $DB->Query($strSql);

		$strItems = "0";
		while($result = $res->Fetch())
		{
			$strItems .= ",". (int)$result['ID'];
		}

		if ($strItems != "0")
		{
			$strSql =
				"UPDATE b_calendar_event SET ".
				$DB->PrepareUpdate("b_calendar_event", array("DELETED" => "Y")).
				" WHERE PARENT_ID in (".$strItems.")";
			$DB->Query($strSql);
		}
		CCalendar::ClearCache(['section_list', 'event_list']);
	}

	public static function CanView($eventId, $userId)
	{
		if ((int)$eventId > 0)
		{
			Loader::includeModule("calendar");
			$event = self::GetList(
				array(
					'arFilter' => array(
						"ID" => $eventId,
					),
					'parseRecursion' => false,
					'fetchAttendees' => true,
					'checkPermissions' => true,
					'userId' => $userId,
				)
			);

			if (!$event || !is_array($event[0]))
			{
				$event = self::GetList(
					array(
						'arFilter' => array(
							"PARENT_ID" => $eventId,
							"CREATED_BY" => $userId,
						),
						'parseRecursion' => false,
						'fetchAttendees' => true,
						'checkPermissions' => true,
						'userId' => $userId,
					)
				);
			}

			// Event is not partly accessible - so it was not cleaned before by ApplyAccessRestrictions
			if (
				$event
				&& is_array($event[0])
				&& ($event[0]['IS_ACCESSIBLE_TO_USER'] ?? null) !== false
				&& (
					isset($event[0]['DESCRIPTION'])
					|| isset($event[0]['IS_MEETING'])
					|| isset($event[0]['LOCATION'])
				)
			)
			{
				return true;
			}

		}

		return false;
	}

	public static function GetEventUserFields($event)
	{
		global $USER_FIELD_MANAGER;

		$eventId = !empty($event['PARENT_ID']) ? (int)$event['PARENT_ID'] : (int)($event['ID'] ?? null);

		if (isset(self::$eventUserFields[$eventId]))
		{
			return self::$eventUserFields[$eventId];
		}

		self::$eventUserFields[$eventId] = $USER_FIELD_MANAGER->GetUserFields('CALENDAR_EVENT', $eventId, LANGUAGE_ID);

		return self::$eventUserFields[$eventId];
	}

	public static function SetExDate($exDate = [], $untilTimestamp = false)
	{
		if ($untilTimestamp && !empty($exDate) && is_array($exDate))
		{
			$exDateRes = [];

			foreach($exDate as $date)
			{
				if (CCalendar::Timestamp($date) <= $untilTimestamp)
				{
					$exDateRes[] = $date;
				}
			}

			$exDate = $exDateRes;
		}

		$exDate = array_unique($exDate);

		return implode(';', $exDate);
	}

	public static function GetEventsByRecId($recurrenceId, $checkPermissions = true, $userId = null)
	{
		if ($recurrenceId > 0)
		{
			$filter = [
				'RECURRENCE_ID' => $recurrenceId,
				'DELETED' => 'N',
			];
			if ($userId)
			{
				$filter['OWNER_ID'] = $userId;
			}

			return self::GetList([
				'arFilter' => $filter,
				'parseRecursion' => false,
				'fetchAttendees' => false,
				'checkPermissions' => $checkPermissions,
				'setDefaultLimit' => false,
			]);
		}
		return [];
	}

	/**
	 * Which events are broken:
	 * 1) events of a series created from the exclusion of another series
	 * 2) exclusions of a series created from the exclusion of another series
	 * 3) events of a series created from the exclusion of another subseries
	 * 4) the exclusions of a series created from the exclusion of another subseries
	 *
	 * this method corrects event comments only when a recurring event is opened, but not when an exception is opened.
	 */
	public static function FixCommentsIfEventIsBroken(array $event): array
	{
		if (self::IsBrokenEventOfSeries($event))
		{
			$commentXmlId = $event['RELATIONS']['COMMENT_XML_ID'];
			$doesntHaveCommentsOrWereCommentsMoved = self::MoveCommentsToFirstRecurrence($event, $commentXmlId);
			if ($doesntHaveCommentsOrWereCommentsMoved)
			{
				self::CleanUpBrokenRecursiveExclusion($commentXmlId);
			}

			$xmlId = $event['RECURRENCE_ID'] ?? null;
			preg_match('/EVENT_\d+_(.*)/', $commentXmlId, $matchesDate);
			$xmlDate = $matchesDate[1] ?? null;

			$doesntHaveCommentsOrWereCommentsMovedAnother = false;
			if (!is_null($xmlId) && !is_null($xmlDate))
			{
				$anotherCommentXmlId = "EVENT_{$xmlId}_{$xmlDate}";
				$doesntHaveCommentsOrWereCommentsMovedAnother = self::MoveCommentsToFirstRecurrence($event, $anotherCommentXmlId);
				if ($doesntHaveCommentsOrWereCommentsMovedAnother)
				{
					self::CleanUpBrokenRecursiveExclusion($anotherCommentXmlId);
				}
			}

			if ($doesntHaveCommentsOrWereCommentsMoved && $doesntHaveCommentsOrWereCommentsMovedAnother)
			{
				self::CleanUpBrokenRecursiveEvent($event);
			}

			unset($event['ORIGINAL_DATE_FROM'], $event['RELATIONS']);

			\CCalendar::ClearCache('event_list');
		}


		return $event;
	}

	public static function IsBrokenEventOfSeries(array $event): bool
	{
		return !empty($event['RRULE']) && !empty($event['RELATIONS']['COMMENT_XML_ID']);
	}

	public static function CleanUpBrokenRecursiveEvent(array $event): void
	{
		$rows = Internals\EventTable::query()
			->setSelect(['ID'])
			->whereNot('RRULE', '')
			->where('PARENT_ID', $event['PARENT_ID'])
		;

		$events = $rows->fetchAll();
		if (empty($events))
		{
			return;
		}

		$eventIds = array_map('intval', array_column($events, 'ID'));
		Internals\EventTable::updateMulti($eventIds, [
			'ORIGINAL_DATE_FROM' => null,
			'RELATIONS' => null,
		]);
	}

	public static function CleanUpBrokenRecursiveExclusion(string $commentXmlId): void
	{
		$rows = Internals\EventTable::query()
			->setSelect(['ID', 'PARENT_ID', 'RECURRENCE_ID', 'ORIGINAL_DATE_FROM'])
			->where('RRULE', '')
			->where('RELATIONS', serialize([
				'COMMENT_XML_ID' => $commentXmlId,
			]))
		;

		$events = $rows->fetchAll();
		foreach ($events as $brokenEvent)
		{
			Internals\EventTable::update($brokenEvent['ID'], [
				'RELATIONS' => serialize([
					'COMMENT_XML_ID' => self::GetEventCommentXmlId($brokenEvent),
				]),
			]);
		}
	}

	public static function MoveCommentsToFirstRecurrence(array $event, string $currentEntityXmlId): bool
	{
		if (!Loader::includeModule('forum'))
		{
			return false;
		}

		$forumId = \CCalendar::GetSettings()['forum_id'];
		$eventEntityId = $event['PARENT_ID'] ?: $event['ID'];
		$newEntityXmlId = "EVENT_$eventEntityId";

		$tsFrom = $event['DATE_FROM_TS_UTC'];
		if (!empty($event['~DATE_FROM']))
		{
			$tsFrom = \CCalendar::TimestampUTC($event['~DATE_FROM']);
		}

		$firstRecurrenceDate = Util::formatDateTimestampUTC($tsFrom);
		if (str_contains($event['EXDATE'], $firstRecurrenceDate))
		{
			$newEntityXmlId = "EVENT_{$event['RECURRENCE_ID']}_{$firstRecurrenceDate}";
		}

		$currentFeed = new \Bitrix\Forum\Comments\Feed($forumId, [
			'type' => 'EV',
			'id' => $eventEntityId,
			'xml_id' => $currentEntityXmlId,
		]);

		return $currentFeed->moveEventCommentsToNewXmlId($newEntityXmlId);
	}

	public static function GetEventCommentXmlId($event)
	{
		if (isset($event['RELATIONS']['ORIGINAL_RECURSION_ID']))
		{
			$date = CCalendar::Date(CCalendar::Timestamp($event['DATE_FROM']), false);
			return "EVENT_{$event['RELATIONS']['ORIGINAL_RECURSION_ID']}_$date";
		}
		if (isset($event['ORIGINAL_DATE_FROM'], $event['RECURRENCE_ID']))
		{
			$date = CCalendar::Date(CCalendar::Timestamp($event['ORIGINAL_DATE_FROM']), false);
			return "EVENT_{$event['RECURRENCE_ID']}_$date";
		}

		$commentXmlId = "EVENT_" . ($event['PARENT_ID'] ?? $event['ID']);

		if (
			self::CheckRecurcion($event)
			&& (!isset($event['RINDEX']) || $event['RINDEX'] > 0)
			&& (CCalendar::Date(CCalendar::Timestamp($event['DATE_FROM']), false)
				!== CCalendar::Date(CCalendar::Timestamp($event['~DATE_FROM'] ?? null), false))
		)
		{
			$commentXmlId .= '_'.CCalendar::Date(CCalendar::Timestamp($event['DATE_FROM']), false);
		}

		return $commentXmlId;
	}

	public static function ExtractDateFromCommentXmlId($xmlId = '')
	{
		$date = false;
		if ($xmlId)
		{
			$xmlAr = explode('_', $xmlId);
			if (is_array($xmlAr) && isset($xmlAr[2]) && $xmlAr[2])
			{
				$date = CCalendar::Date(CCalendar::Timestamp($xmlAr[2]), false);
			}
		}
		return $date;
	}

	public static function GetRRULEDescription($event, $html = false, $showUntil = true, $languageId = null)
	{
		$res = '';
		if (!empty($event['RRULE']))
		{
			$event['RRULE'] = self::ParseRRULE($event['RRULE']);

			if (!empty($event['RRULE']['BYDAY']))
			{
				$event['RRULE']['BYDAY'] = self::sortByDay($event['RRULE']['BYDAY']);
			}

			switch($event['RRULE']['FREQ'])
			{
				case 'DAILY':
					if ((int)$event['RRULE']['INTERVAL'] === 1)
					{
						$res = Loc::getMessage('EC_RRULE_EVERY_DAY', null, $languageId);
					}
					else
					{
						$res = Loc::getMessage(
							'EC_RRULE_EVERY_DAY_1',
							['#DAY#' => $event['RRULE']['INTERVAL']],
							$languageId
						);
					}
					break;
				case 'WEEKLY':
					$daysList = [];
					foreach ($event['RRULE']['BYDAY'] as $day)
					{
						$daysList[] = Loc::getMessage('EC_'.$day, null, $languageId);
					}

					$daysList = implode(', ', $daysList);
					if ((int)$event['RRULE']['INTERVAL'] === 1)
					{
						$res = Loc::getMessage(
							'EC_RRULE_EVERY_WEEK',
							['#DAYS_LIST#' => $daysList],
							$languageId
						);
					}
					else
					{
						$res = Loc::getMessage(
							'EC_RRULE_EVERY_WEEK_1',
							[
								'#WEEK#' => $event['RRULE']['INTERVAL'],
								'#DAYS_LIST#' => $daysList,
							],
							$languageId
						);
					}
					break;
				case 'MONTHLY':
					if ((int)$event['RRULE']['INTERVAL'] === 1)
					{
						$res = Loc::getMessage('EC_RRULE_EVERY_MONTH', null, $languageId);
					}
					else
					{
						$res = Loc::getMessage(
							'EC_RRULE_EVERY_MONTH_1',
							[
								'#MONTH#' => $event['RRULE']['INTERVAL'],
							],
							$languageId
						);
					}
					break;
				case 'YEARLY':
					$fromTs = CCalendar::Timestamp($event['DATE_FROM']);
					if ($event['DT_SKIP_TIME'] !== "Y")
					{
						$fromTs -= $event['~USER_OFFSET_FROM'] ?? 0;
					}

					if ((int)$event['RRULE']['INTERVAL'] === 1)
					{
						$res = Loc::getMessage(
							'EC_RRULE_EVERY_YEAR',
							[
								'#DAY#' => FormatDate('j', $fromTs, false, $languageId), // day
								'#MONTH#' => FormatDate('n', $fromTs, false, $languageId), // month
							],
							$languageId
						);
					}
					else
					{
						$res = Loc::getMessage(
							'EC_RRULE_EVERY_YEAR_1',
							[
								'#YEAR#' => $event['RRULE']['INTERVAL'],
								'#DAY#' => FormatDate('j', $fromTs, false, $languageId), // day
								'#MONTH#' => FormatDate('n', $fromTs, false, $languageId), // month
							],
							$languageId
						);
					}
					break;
			}

			if ($html)
			{
				$res .= '<br>';
			}
			else
			{
				$res .= ', ';
			}

			if (isset($event['~DATE_FROM']))
			{
				$res .= Loc::getMessage(
					'EC_RRULE_FROM',
					['#FROM_DATE#' => CCalendar::Date(CCalendar::Timestamp($event['~DATE_FROM']), false)],
					$languageId
				);
			}
			else
			{
				$res .= Loc::getMessage(
					'EC_RRULE_FROM',
					['#FROM_DATE#' => CCalendar::Date(CCalendar::Timestamp($event['DATE_FROM']), false)],
					$languageId
				);
			}

			if ($showUntil && ($event['RRULE']['UNTIL'] ?? null) != CCalendar::GetMaxDate())
			{
				$res .= ' ' . Loc::getMessage(
					'EC_RRULE_UNTIL',
					['#UNTIL_DATE#' => CCalendar::Date(CCalendar::Timestamp($event['RRULE']['UNTIL']), false)],
					$languageId
				);
			}
			elseif ($showUntil && (($event['RRULE']['COUNT'] ?? null) > 0))
			{
				$res .= ', ' . Loc::getMessage(
					'EC_RRULE_COUNT',
					['#COUNT#' => $event['RRULE']['COUNT']],
					$languageId
				);
			}
		}

		return $res;
	}

	public static function ExcludeInstance($eventId, $excludeDate)
	{
		global $CACHE_MANAGER;

		$userId = \CCalendar::getCurUserId();
		$eventId = (int)$eventId;
		$excludeDateTs = CCalendar::Timestamp($excludeDate);
		$excludeDate = CCalendar::Date($excludeDateTs, false);

		$event = self::GetList([
			'arFilter' => [
				"ID" => $eventId,
				"DELETED" => "N",
			],
			'parseRecursion' => false,
			'fetchAttendees' => true,
			'setDefaultLimit' => false,
		]);
		if ($event && is_array($event[0]))
		{
			$event = $event[0];
		}

		if ($event && $excludeDate && self::CheckRecurcion($event))
		{
			$excludeDates = self::GetExDate($event['EXDATE']);
			$excludeDates[] = $excludeDate;

			$id = CCalendar::SaveEvent([
				'arFields' => [
					'ID' => $event['ID'],
					'DATE_FROM' => $event['DATE_FROM'],
					'DATE_TO' => $event['DATE_TO'],
					'EXDATE' => self::SetExDate($excludeDates),
				],
				'silentErrorMode' => false,
				'recursionEditMode' => 'skip',
				'editParentEvents' => true,
			]);

			if (!empty($event['ATTENDEE_LIST']) && is_array($event['ATTENDEE_LIST']))
			{
				foreach($event['ATTENDEE_LIST'] as $attendee)
				{
					if ($attendee['status'] === 'Y')
					{
						if ($event['DT_SKIP_TIME'] !== 'Y')
						{
							$excludeDate = CCalendar::Date(CCalendar::DateWithNewTime(CCalendar::Timestamp($event['DATE_FROM']), $excludeDateTs));
						}

						$CACHE_MANAGER->ClearByTag('calendar_user_'.$attendee["id"]);

						$meetingHost = $event['MEETING']['MEETING_CREATOR'] ?? $event['MEETING_HOST'];
						CCalendarNotify::Send([
							"mode" => 'cancel_this',
							"name" => $event['NAME'],
							"from" => $excludeDate,
							"guestId" => $attendee["id"],
							"eventId" => $event['PARENT_ID'],
							"userId" => $userId > 0 ? $userId : $meetingHost,
							"fields" => $event,
						]);
					}
				}
			}
		}
	}

	public static function getDiskUFFileNameList($valueList = [])
	{
		$result = [];

		if (
			!empty($valueList)
			&& is_array($valueList)
			&& Loader::includeModule('disk')
		)
		{
			$attachedIdList = [];
			foreach($valueList as $value)
			{
				[$type, $realValue] = FileUserType::detectType($value);
				if ($type === FileUserType::TYPE_NEW_OBJECT)
				{
					$file = \Bitrix\Disk\File::loadById($realValue, array('STORAGE'));
					$result[] = strip_tags($file->getName());
				}
				else
				{
					$attachedIdList[] = $realValue;
				}
			}

			if (!empty($attachedIdList))
			{
				$attachedObjects = AttachedObject::getModelList(array(
					'with' => array('OBJECT'),
					'filter' => array(
						'ID' => $attachedIdList,
					),
				));
				foreach($attachedObjects as $attachedObject)
				{
					$file = $attachedObject->getFile();
					$result[] = strip_tags($file->getName());
				}
			}
		}

		return $result;
	}

	public static function getSearchIndexContent($eventId)
	{
		$res = '';
		if ((int)$eventId > 0)
		{
			$event = Internals\EventTable::query()
				->setSelect(['ID', 'DELETED', 'SEARCHABLE_CONTENT'])
				->where('ID', (int)$eventId)
				->where('DELETED', 'N')
				->exec()->fetch()
			;

			$res = !empty($event['SEARCHABLE_CONTENT']) ? $event['SEARCHABLE_CONTENT'] : '';
		}

		return $res;
	}

	public static function updateSearchIndex($eventIdList = [], $params = []): void
	{
		if (isset($params['isNew']) && $params['isNew'] === false)
		{
			$targetChangedFields = ['NAME', 'DESCRIPTION', 'LOCATION'];

			if (
				isset($params['changedFields'])
				&& empty(array_intersect($params['changedFields'], $targetChangedFields))
			)
			{
				return;
			}
		}

		if (isset($params['events']))
		{
			$events = $params['events'];
		}
		else
		{
			$events = self::getList([
				'arFilter' => [
					'ID' => $eventIdList,
					'DELETED' => 'N',
				],
				'parseRecursion' => false,
				'fetchAttendees' => true,
				'checkPermissions' => false,
				'setDefaultLimit' => false,
				'userId' => $params['userId'] ?? null,
			]);
		}

		if (is_array($events))
		{
			$event = current($events);
			$eventId = (int)$event['ID'];
			$content = self::formatSearchIndexContent($event);

			if (!empty($params['updateAllByParent']) && $eventId === (int)$event['PARENT_ID'])
			{
				Internals\EventTable::updateByFilter(
					['=PARENT_ID' => $eventId],
					['SEARCHABLE_CONTENT' => $content]
				);
			}
			else
			{
				Internals\EventTable::update($eventId, [
					['SEARCHABLE_CONTENT' => $content],
				]);
			}
		}
	}

	public static function formatSearchIndexContent($entry = []): string
	{
		$content = '';
		if (!empty($entry))
		{
			$content = static::prepareToken(
				Emoji::encode($entry['NAME'])
				. ' '
				. Emoji::encode($entry['DESCRIPTION'])
				. ' '
				. Emoji::encode(\CCalendar::GetTextLocation($entry['LOCATION'] ?? ''))
			);

			if ($entry['IS_MEETING'])
			{
				$attendeesWereHandled = false;
				if (!empty($entry['ATTENDEE_LIST']) && is_array($entry['ATTENDEE_LIST']))
				{
					foreach($entry['ATTENDEE_LIST'] as $attendee)
					{
						if (isset(self::$userIndex[$attendee['id']]))
						{
							$content .= ' '.static::prepareToken(self::$userIndex[$attendee['id']]['DISPLAY_NAME']);
						}
					}
					$attendeesWereHandled = true;
				}

				if (!empty($entry['ATTENDEES_CODES']))
				{
					if ($attendeesWereHandled)
					{
						$attendeesCodes = [];
						foreach($entry['ATTENDEES_CODES'] as $code)
						{
							if (!str_starts_with($code, 'U'))
							{
								$attendeesCodes[] = $code;
							}
						}
					}
					else
					{
						$attendeesCodes = $entry['ATTENDEES_CODES'];
					}
					$content .= ' ' . static::prepareToken(
						implode(
							' ',
							Bitrix\Socialnetwork\Item\LogIndex::getEntitiesName($attendeesCodes)
						)
					);
				}
			}
			else
			{
				$content .= ' ' . static::prepareToken(CCalendar::GetUserName($entry['CREATED_BY']));
			}

			try
			{
				if (!empty($entry['UF_WEBDAV_CAL_EVENT']) && Loader::includeModule('disk'))
				{
					$fileNameList = self::getDiskUFFileNameList($entry['UF_WEBDAV_CAL_EVENT']);
					if (!empty($fileNameList))
					{
						$content .= ' '.static::prepareToken(implode(' ', $fileNameList));
					}
				}
			}
			catch (RuntimeException $e)
			{
			}

			try
			{
				if (!empty($entry['UF_CRM_CAL_EVENT']) && Loader::includeModule('crm'))
				{
					$uf = $entry['UF_CRM_CAL_EVENT'];

					foreach ($uf as $item)
					{
						$title = self::getCrmElementTitle($item);
						$content .= ' ' . static::prepareToken($title);
					}
				}
			}
			catch (RuntimeException $e)
			{
			}
		}

		return $content;
	}

	public static function getCrmElementTitle(string $item)
	{
		$crmElement = explode('_', $item);
		$type = $crmElement[0];
		$typeId = \CCrmOwnerType::ResolveID(\CCrmOwnerTypeAbbr::ResolveName($type));

		return \CCrmOwnerType::GetCaption($typeId, $crmElement[1]);
	}

	public static function GetCount()
	{
		global $DB;
		$count = 0;
		$res = $DB->Query('select count(*) as c from b_calendar_event');

		if ($res = $res->Fetch())
		{
			$count = $res['c'];
		}

		return $count;
	}

	public static function updateColor($eventId, $color = '')
	{
		Internals\EventTable::update((int)$eventId, ['COLOR' => $color]);

		$eventAttendeeId = self::getEventAttendeeIdByEventIdAndUserId($eventId, CCalendar::GetCurUserId());
		if ($eventAttendeeId)
		{
			self::updateEventAttendeeColor($eventAttendeeId, $color);
		}
	}

	/**
	 * Applies ROT13 transform to search token, in order to bypass default mysql search blacklist.
	 * @param string $token Search token.
	 * @return string
	 */
	public static function prepareToken($token)
	{
		return str_rot13($token);
	}

	public static function isFullTextIndexEnabled()
	{
		return COption::GetOptionString("calendar", "~ft_b_calendar_event", false);
	}

	public static function getUserIndex()
	{
		return self::$userIndex;
	}

	public static function getEventForViewInterface($entryId, $params = [])
	{
		$params['eventDate'] = ($params['eventDate'] ?? null);
		$params['timezoneOffset'] = ($params['timezoneOffset'] ?? null);
		$params['userId'] = ($params['userId'] ?? null);

		$fromTs = \CCalendar::Timestamp($params['eventDate'], true, false) - $params['timezoneOffset'];
		$userDateFrom = \CCalendar::Date($fromTs);

		$entry = self::GetList([
			'arFilter' => [
				"ID" => $entryId,
				"DELETED" => "N",
				"FROM_LIMIT" => $userDateFrom,
				"TO_LIMIT" => $userDateFrom,
			],
			'parseRecursion' => true,
			'maxInstanceCount' => 1,
			'preciseLimits' => true,
			'fetchAttendees' => true,
			'checkPermissions' => true,
			'setDefaultLimit' => false,
			'getUserfields' => true,
		]);

		if (
			$params['eventDate']
			&& isset($entry[0])
			&& is_array($entry[0])
			&& $entry[0]['RRULE']
			&& $entry[0]['EXDATE']
			&& in_array($params['eventDate'], self::GetExDate($entry[0]['EXDATE']))
		)
		{
			$entry = self::GetList([
                'arFilter' => [
                    "RECURRENCE_ID" => $entryId,
                    "DELETED" => "N",
                    "FROM_LIMIT" => $params['eventDate'],
                    "TO_LIMIT" => $params['eventDate'],
                ],
                'parseRecursion' => true,
                'maxInstanceCount' => 1,
                'preciseLimits' => true,
                'fetchAttendees' => true,
                'checkPermissions' => true,
                'setDefaultLimit' => false,
                'getUserfields' => true,
            ]);
		}

		if (!$entry || !is_array($entry[0]))
		{
			$entry = self::GetList([
				'arFilter' => [
					"ID" => $entryId,
					"DELETED" => "N",
				],
				'parseRecursion' => true,
				'maxInstanceCount' => 1,
				'fetchAttendees' => true,
				'checkPermissions' => true,
				'setDefaultLimit' => false,
                'getUserfields' => true,
			]);
		}

		// Here we can get events with wrong RRULE ('parseRecursion' => false)
		if (!$entry || !is_array($entry[0]))
		{
			$entry = self::GetList([
				'arFilter' => [
					"ID" => $entryId,
					"DELETED" => "N",
				],
				'parseRecursion' => false,
				'fetchAttendees' => true,
				'checkPermissions' => true,
				'setDefaultLimit' => false,
                'getUserfields' => true,
			]);
		}

		if ($entry && is_array($entry[0]))
		{
			$entry = $entry[0];
			if ($entry['IS_MEETING'] && (int)$entry['PARENT_ID'] !== (int)$entry['ID'])
			{
				$parentEntry = false;
				$parentEntryList = self::GetList([
					'arFilter' => [
						"ID" => (int)$entry['PARENT_ID'],
						"FROM_LIMIT" => $userDateFrom,
						"TO_LIMIT" => $userDateFrom,
					],
					'parseRecursion' => true,
					'maxInstanceCount' => 1,
					'preciseLimits' => true,
					'fetchAttendees' => true,
					'checkPermissions' => true,
					'setDefaultLimit' => false,
                    'getUserfields' => true,

				]);

				if (!empty($parentEntryList[0]) && is_array($parentEntryList[0]))
				{
					$parentEntry = $parentEntryList[0];
				}

				if ($parentEntry)
				{
					if ($parentEntry['DELETED'] === 'Y')
					{
						self::CleanEventsWithDeadParents();
						$entry = false;
					}

					if ((int)$parentEntry['MEETING_HOST'] === (int)$params['userId'])
					{
						$entry = $parentEntry;
					}
				}
			}

			if (
				isset($entry['UF_WEBDAV_CAL_EVENT'])
				&& is_array($entry['UF_WEBDAV_CAL_EVENT'])
				&& empty($entry['UF_WEBDAV_CAL_EVENT'])
			)
			{
				$entry['UF_WEBDAV_CAL_EVENT'] = null;
			}
		}

		if (
			($entry['IS_MEETING'] ?? null)
			&& !empty($entry['ATTENDEE_LIST'])
			&& is_array($entry['ATTENDEE_LIST'])
			&& (int)$entry['CREATED_BY'] !== $params['userId']
			&& (int)$entry['OWNER_ID'] !== $params['userId']
			&& ($params['recursion'] ?? null) !== false
			&& ($entry['CAL_TYPE'] !== Dictionary::CALENDAR_TYPE['open_event'])
		)
		{
			foreach($entry['ATTENDEE_LIST'] as $attendee)
			{
				if ((int)$attendee['id'] === (int)$params['userId'])
				{
					$entry = self::GetList([
						'arFilter' => [
							'PARENT_ID' => $entry['PARENT_ID'],
							'OWNER_ID' => $params['userId'],
							'DELETED' => 'N',
						],
						'parseRecursion' => false,
						'maxInstanceCount' => 1,
						'preciseLimits' => false,
						'fetchAttendees' => false,
						'checkPermissions' => true,
						'setDefaultLimit' => false,
                        'getUserfields' => true,
					]);

					if ($entry && is_array($entry[0]) && $entry[0]['CAL_TYPE'] === 'location')
					{
						$params['recursion'] = false;
						$entry = self::getEventForViewInterface($entry[0]['PARENT_ID'], $params);
					}
					else if ($entry && is_array($entry[0]))
					{
						$params['recursion'] = false;
						$entry = self::getEventForViewInterface($entry[0]['ID'], $params);
					}
				}
			}
		}

		return $entry;
	}

	public static function getEventForEditInterface($entryId, $params = [])
	{
		$entry = self::GetList(
			[
				'arFilter' => [
					"ID" => $entryId,
					"DELETED" => "N",
					"FROM_LIMIT" => $params['eventDate'] ?? null,
					"TO_LIMIT" => $params['eventDate'] ?? null,
				],
				'parseRecursion' => true,
				'maxInstanceCount' => 1,
				'preciseLimits' => true,
				'fetchAttendees' => true,
				'checkPermissions' => true,
				'setDefaultLimit' => false,
			]
		);

		if (!$entry || !is_array($entry[0]))
		{
			$entry = self::GetList(
				[
					'arFilter' => [
						"ID" => $entryId,
						"DELETED" => "N",
					],
					'parseRecursion' => true,
					'maxInstanceCount' => 1,
					'fetchAttendees' => true,
					'checkPermissions' => true,
					'setDefaultLimit' => false,
				]
			);
		}

		// Here we can get events with wrong RRULE ('parseRecursion' => false)
		if (!$entry || !is_array($entry[0]))
		{
			$entry = self::GetList(
				[
					'arFilter' => [
						"ID" => $entryId,
						"DELETED" => "N",
					],
					'parseRecursion' => false,
					'fetchAttendees' => true,
					'checkPermissions' => true,
					'setDefaultLimit' => false,
				]
			);
		}

		$entry = is_array($entry) ? $entry[0] : null;

		if (is_array($entry) && $entry['ID'] !== $entry['PARENT_ID'])
		{
			return self::getEventForEditInterface($entry['PARENT_ID'], $params);
		}

		return $entry;
	}

	public static function handleAccessCodes($accessCodes = [], $params = [])
	{
		$accessCodes = is_array($accessCodes) ? $accessCodes : [];
		$userId = isset($params['userId']) ? $params['userId'] : \CCalendar::getCurUserId();

		if (empty($accessCodes))
		{
			$accessCodes[] = 'U'.$userId;
		}

		$accessCodes = array_unique($accessCodes);

		return $accessCodes;
	}

	/**
	 * @param mixed $entryFields
	 * @param array $params
	 * @param mixed $currentEvent
	 * @param int $userId
	 * @throws Rooms\OccupancyCheckerException
	 */
	public static function checkLocationOccupancy(
		mixed $entryFields,
		array $params,
		mixed $currentEvent,
		int $userId
	)
	{
		$entryFields['TZ_FROM'] ??= null;
		$entryFields['TZ_TO'] ??= null;
		$entryFields['TZ_OFFSET_FROM'] ??= 0;
		$entryFields['TZ_OFFSET_TO'] ??= 0;

		$fieldsToCheckOccupancy = $entryFields;
		if (!empty($params['checkLocationOccupancyFields']))
		{
			$fieldsToCheckOccupancy = $params['checkLocationOccupancyFields'];
			$fieldsToCheckOccupancy['LOCATION'] = [
				'NEW' => $fieldsToCheckOccupancy['LOCATION'] ?? '',
			];
			self::CheckFields($fieldsToCheckOccupancy, $currentEvent, $userId);
		}

		$occupancyCheckResult = (new Rooms\OccupancyChecker())->check($fieldsToCheckOccupancy);
		if (!$occupancyCheckResult->isSuccess())
		{
			$disturbingEventsFormatted = $occupancyCheckResult->getData()['disturbingEventsFormatted'];
			if ($occupancyCheckResult->getData()['isDisturbingEventsAmountOverShowLimit'])
			{
				$message = Loc::getMessage('EC_LOCATION_REPEAT_BUSY_TOO_MANY', [
					'#DATES#' => $disturbingEventsFormatted,
				]);
			}
			else
			{
				$message = Loc::getMessage('EC_LOCATION_REPEAT_BUSY', [
					'#DATES#' => $disturbingEventsFormatted,
				]);
			}

			throw new Rooms\OccupancyCheckerException($message);
		}
	}

	/**
	 * @param bool $isNewEvent
	 * @param mixed $entryFields
	 * @param int $userId
	 * @param mixed $currentEvent
	 * @return mixed
	 */
	public static function tryingToFindEventSection(
		bool $isNewEvent,
		mixed $entryFields,
		int $userId,
		mixed $currentEvent
	): mixed
	{
		// It's new event we have to find section where to put it automatically
		if ($isNewEvent)
		{
			if (
				!empty($entryFields['IS_MEETING'])
				&& !empty($entryFields['PARENT_ID'])
				&& ($entryFields['CAL_TYPE'] ?? null) === 'user'
			)
			{
				$sectionId = CCalendar::GetMeetingSection($entryFields['OWNER_ID'] ?? null);
			}
			else
			{
				$sectionId = CCalendarSect::GetLastUsedSection(
					$entryFields['CAL_TYPE'] ?? null, $entryFields['OWNER_ID'] ?? null, $userId
				);
			}

			if ($sectionId)
			{
				$res = CCalendarSect::GetList([
					'arFilter' => [
						'CAL_TYPE' => $entryFields['CAL_TYPE'] ?? null, 'OWNER_ID' => $entryFields['OWNER_ID'] ?? null,
						'ID' => $sectionId,
					],
				]);

				if (empty($res[0]))
				{
					$sectionId = false;
				}
			}
			else
			{
				$sectionId = false;
			}

			if (empty($sectionId))
			{
				$sectRes = CCalendarSect::GetSectionForOwner($entryFields['CAL_TYPE'], $entryFields['OWNER_ID'], true);
				$sectionId = $sectRes['sectionId'];
			}
		}
		else
		{
			$sectionId = $currentEvent['SECTION_ID'] ?? $currentEvent['SECT_ID'];
		}

		return $sectionId;
	}

	/**
	 * @param array $involvedAttendees
	 * @param $meetingHost
	 * @return array
	 * @throws \Bitrix\Main\ArgumentException
	 * @throws \Bitrix\Main\ObjectPropertyException
	 * @throws \Bitrix\Main\SystemException
	 */
	public static function generateUserIndex(array $involvedAttendees, $meetingHost): array
	{
		$userIndex = [];

		// Here we collecting information about EXTERNAL_AUTH_ID to
		// know if some of the users are external
		$orm = UserTable::getList([
			'filter' => [
				'=ID' => $involvedAttendees, '=ACTIVE' => 'Y',
			], 'select' => [
				'ID', 'EXTERNAL_AUTH_ID', 'NAME', 'LAST_NAME', 'SECOND_NAME', 'LOGIN', 'EMAIL', 'TITLE',
				'UF_DEPARTMENT',
			],
		]);

		while ($user = $orm->fetch())
		{
			if ($user['ID'] === ($meetingHost ?? null))
			{
				$user['STATUS'] = 'accepted';
			}
			else
			{
				$user['STATUS'] = 'needs_action';
			}

			$userIndex[$user['ID']] = $user;
		}

		return $userIndex;
	}

	/**
	 * @param string|null $serializedMeetingInfo
	 * @return string|null
	 */
	private static function getSenderEmailForIcal(string $serializedMeetingInfo = null): ?string
	{
		$meetingInfo = unserialize($serializedMeetingInfo, ['allowed_classes' => false]);

		return !empty($meetingInfo) && !empty($meetingInfo['MAIL_FROM'])
			? $meetingInfo['MAIL_FROM']
			: null;
	}

	/**
	 * @param $userIndex
	 * @param $organizerId
	 * @return array|null
	 * @throws \Bitrix\Main\ArgumentException
	 * @throws \Bitrix\Main\ObjectPropertyException
	 * @throws \Bitrix\Main\SystemException
	 */
	private static function getSenderForIcal($userIndex, $organizerId): ?array
	{
		if (!empty($userIndex) && !empty($userIndex[$organizerId]))
		{
			return $userIndex[$organizerId];
		}

		$userOrm = UserTable::getList([
			'filter' => [
				'=ID' => $organizerId,
				'=ACTIVE' => 'Y',
			],
			'select' => [
				'ID',
				'EXTERNAL_AUTH_ID',
				'NAME',
				'LAST_NAME',
				'SECOND_NAME',
				'LOGIN',
				'EMAIL',
				'TITLE',
				'UF_DEPARTMENT',
			],
		]);

		if ($user = $userOrm->fetch())
		{
			return $user;
		}

		return null;
	}

	/**
	 * @deprecated
	 * @param array $event
	 * @param array $fields
	 */
	public static function updateEventFields(array $event, array $fields): void
	{
		global $DB;

		if (!$fields)
		{
			return;
		}

		CTimeZone::Disable();

		$strSql = "UPDATE b_calendar_event SET ".
			$DB->PrepareUpdate("b_calendar_event", $fields)
			. " WHERE ID=" . (int)$event['ID'] . "; ";
		$DB->Query($strSql);


		CTimeZone::Enable();
	}

	/**
	 * @deprecated
	 * @param int $eventId
	 * @param string $status
	 */
	public static function updateSyncStatus(int $eventId, string $status): void
	{
		global $DB;

		if (in_array($status, Bitrix\Calendar\Sync\Google\Dictionary::SYNC_STATUS, true))
		{
			$DB->Query(
				"UPDATE b_calendar_event"
				. " SET " . $DB->PrepareUpdate('b_calendar_event', ['SYNC_STATUS' => $status])
				. " WHERE ID = " . $eventId . ";"
			);
		}
	}

	public static function checkLocationField($location, $isNewEvent)
	{
		$parsedNew = Bitrix\Calendar\Rooms\Util::parseLocation($location['NEW']);
		if (!empty($parsedNew['room_event_id']))
		{
			$location['NEW'] = 'calendar_' . $parsedNew['room_id'];
		}

		if ($isNewEvent)
		{
			$location['OLD'] = '';
		}

		return $location;
	}

	/**
	 * @param array $childParams
	 * @return void
	 */
	public static function prepareArFieldBeforeSyncEvent(array &$childParams): void
	{
		if (is_string($childParams['arFields']['MEETING']))
		{
			$childParams['arFields']['MEETING'] = unserialize($childParams['arFields']['MEETING'], ['allowed_classes' => false]);
		}

		$childParams['arFields']['MEETING']['LANGUAGE_ID'] = CCalendar::getUserLanguageId((int)$childParams['arFields']['OWNER_ID']);
	}

	/**
	 * @param array $childParams
	 * @return string
	 * @throws \Bitrix\Main\ObjectException
	 */
	private static function getUidForChildEvent(array $event): string
	{
		return UidGenerator::createInstance()
			->setPortalName(Util::getServerName())
			->setDate(new Date(
				Util::getDateObject(
					$event['DATE_FROM'],
					($event['SKIP_TIME'] ?? false),
					$event['TZ_FROM'] ?? null,
				)
			))
			->setUserId((int)$event['OWNER_ID'])
			->getUidWithDate()
		;
	}

	/**
	 * @param array $eventData
	 * @param array $params
	 *
	 * @return void
	 * @throws \Bitrix\Calendar\Core\Base\BaseException
	 * @throws \Bitrix\Main\ArgumentException
	 * @throws \Bitrix\Main\ObjectException
	 * @throws \Bitrix\Main\ObjectNotFoundException
	 * @throws \Bitrix\Main\ObjectPropertyException
	 * @throws \Bitrix\Main\SystemException
	 * @throws \Bitrix\Main\LoaderException
	 */
	private static function onEventDelete(
		array $eventData,
		array $params = []
	): void
	{
		/** @var \Bitrix\Calendar\Core\Mappers\Factory $mapperFactory */
		$mapperFactory = \Bitrix\Main\DI\ServiceLocator::getInstance()->get('calendar.service.mappers.factory');
		$exDate = null;
		$originalDate = null;
		if (empty($eventData))
		{
			return;
		}
		/** @var Section $section */
		$section = $mapperFactory->getSection()->getById((int)$eventData['SECTION_ID']);

		if ($section === null)
		{
			return;
		}

		$factories = FactoriesCollection::createBySection($section);

		if ($factories->count() === 0)
		{
			return;
		}

		$recId = $eventData['RECURRENCE_ID'];
		if ($recId)
		{
			$exDate = $eventData['DATE_FROM'];
			$originalDate = $eventData['ORIGINAL_DATE_FROM'];
			$originalEventData = $eventData;
			$eventData = Internals\EventTable::getRow([
				'filter' => [
					'=PARENT_ID' => $eventData['RECURRENCE_ID'],
					'=OWNER_ID' => $eventData['OWNER_ID'],
					'=DELETED' => 'N',
				],
			]);
		}
		if ($eventData)
		{
			$event = (new \Bitrix\Calendar\Core\Builders\EventBuilderFromArray($eventData))->build();
		}
		else
		{
			return;
		}

		$syncManager = new Synchronization($factories);
		$context = new Context([]);

		// TODO: there's a dependence of the general on the particular
		if (
			!empty($params['originalFrom'])
			&& (int)($params['userId'] ?? null) === (int)$eventData['OWNER_ID']
		)
		{
			$context->add('sync', 'originalFrom', $params['originalFrom']);
			$connection = $mapperFactory->getConnection()->getMap([
				'=ACCOUNT_TYPE' => $params['originalFrom'],
				'=ENTITY_TYPE' => $event->getCalendarType(),
				'=ENTITY_ID' => $event->getOwner()?->getId(),
			])->fetch();
			if ($connection)
			{
				$syncManager->upEventVersion(
					$event,
					$connection,
					$arFields['VERSION'] ?? 1
				);
			}
		}

		if ($exDate)
		{
			$exDate = new \Bitrix\Main\Type\Date(CCalendar::Date(CCalendar::Timestamp($exDate), false));
			$context->add('sync', 'excludeDate', new \Bitrix\Calendar\Core\Base\Date($exDate));
		}

		if ($originalDate)
		{
			$originalDate = new \Bitrix\Main\Type\DateTime(CCalendar::Date(CCalendar::Timestamp($originalDate)));
			$context->add('sync', 'originalDate', new \Bitrix\Calendar\Core\Base\Date($originalDate));
		}

		if ($recId)
		{
			$result = $syncManager->deleteInstance($event, $context);
			if (!empty($originalEventData) && !$result->isSuccess())
			{
				$originalEvent = (new \Bitrix\Calendar\Core\Builders\EventBuilderFromArray($originalEventData))->build();
				$syncManager->deleteEvent($originalEvent, $context);
			}
		}
		else
		{
			$syncManager->deleteEvent($event, $context);
		}
	}

	private static function isNewAttendee($attendees, $currentId): bool
	{
		foreach ($attendees as $attendee)
		{
			if ((int)$attendee['id'] === (int)$currentId)
			{
				return false;
			}
		}

		return true;
	}

	public static function sortByDay(array $byDay)
	{
		uasort($byDay, function($a, $b){
			$map = [
				'MO' => 0,
				'TU' => 1,
				'WE' => 2,
				'TH' => 3,
				'FR' => 4,
				'SA' => 5,
				'SU' => 6,
			];

			return $map[$a] < $map[$b] ? -1 : 1;
		});

		return $byDay;
	}

	/**
	 * @param array $arFields
	 * @param int $toTs
	 * @param $currentExDate
	 *
	 * @return void
	 */
	private static function checkRecurringRuleField(array &$arFields, int $toTs, $currentExDate): void
	{
		// Check rrules
		if (
			!empty($arFields['RRULE']['FREQ'])
			&& in_array($arFields['RRULE']['FREQ'], ['HOURLY', 'DAILY', 'MONTHLY', 'YEARLY', 'WEEKLY'])
		)
		{
			// Interval
			if (isset($arFields['RRULE']['INTERVAL']) && (int)$arFields['RRULE']['INTERVAL'] > 1)
				$arFields['RRULE']['INTERVAL'] = (int)$arFields['RRULE']['INTERVAL'];

			// Until date

			$untilTs = CCalendar::Timestamp($arFields['RRULE']['UNTIL'] ?? null, false, false);
			if (isset($arFields['RRULE']['COUNT']))
			{
				$arFields['RRULE']['COUNT'] = (int)$arFields['RRULE']['COUNT'];
			}

			if (!$untilTs)
			{
				$arFields['RRULE']['UNTIL'] = CCalendar::GetMaxDate();
				$untilTs = CCalendar::Timestamp($arFields['RRULE']['UNTIL'], false, false);
			}
			elseif ($untilTs + CCalendar::GetDayLen() < $toTs)
			{
				$untilTs = $toTs;
			}

			$arFields['DATE_TO_TS_UTC'] = $untilTs + CCalendar::GetDayLen();
			if (isset($arFields['RRULE']['COUNT']))
			{
				$arFields['DATE_TO_TS_UTC'] = self::calculateUntilForCountRRule($arFields);
			}

			$arFields['RRULE']['UNTIL'] = CCalendar::Date($untilTs, false);
			unset($arFields['RRULE']['~UNTIL']);

			if (isset($arFields['RRULE']['BYDAY']))
			{
				if (is_array($arFields['RRULE']['BYDAY']))
				{
					$BYDAY = $arFields['RRULE']['BYDAY'];
				}
				else
				{
					$BYDAY = [];
					$days = ['SU', 'MO', 'TU', 'WE', 'TH', 'FR', 'SA'];
					$bydays = explode(',', $arFields['RRULE']['BYDAY']);
					foreach ($bydays as $day)
					{
						$day = mb_strtoupper($day);
						if (in_array($day, $days, true))
						{
							$BYDAY[] = $day;
						}
					}
				}
				$arFields['RRULE']['BYDAY'] = implode(',', $BYDAY);
			}

			if (isset($arFields["EXDATE"]))
			{
				$excludeDates = self::GetExDate($arFields["EXDATE"]);
			}
			else
			{
				$excludeDates = self::GetExDate($currentExDate);
			}

			if (!empty($excludeDates) && $untilTs)
			{
				$arFields["EXDATE"] = self::SetExDate($excludeDates, $untilTs);
			}

			$arFields['RRULE'] = self::PackRRule($arFields['RRULE']);
		}
		else
		{
			$arFields['RRULE'] = '';
			$arFields['EXDATE'] = '';
		}
	}

	protected static function calculateUntilForCountRRule(array $arFields, int $offsetTs = 0): int
	{
		$rrule = self::ParseRRULE($arFields['RRULE']);
		$interval = (int)$rrule['INTERVAL'];
		$count = (int)$rrule['COUNT'];

		$fromTS = CCalendar::Timestamp($arFields['DATE_FROM']) + $offsetTs;
		$toTS = CCalendar::Timestamp($arFields['DATE_TO']) + $offsetTs;
		$length = $toTS - $fromTS;

		$h = (int)date('H', $toTS);
		$min = (int)date('i', $toTS);
		$d = (int)date('d', $toTS);
		$m = (int)date('m', $toTS);
		$y = (int)date('Y', $toTS);

		if ($rrule['FREQ'] === 'DAILY')
		{
			return mktime($h, $min, 0, $m, $d + $count * $interval, $y);
		}
		if ($rrule['FREQ'] === 'MONTHLY')
		{
			return mktime($h, $min, 0, $m + $count * $interval, $d, $y);
		}
		if ($rrule['FREQ'] === 'YEARLY')
		{
			return mktime($h, $min, 0, $m, $d, $y + $count * $interval);
		}
		if ($rrule['FREQ'] === 'WEEKLY')
		{
			$byDay = $rrule['BYDAY'];

			if (!is_array($byDay))
			{
				$days = [];
				$weekDays = ['SU', 'MO', 'TU', 'WE', 'TH', 'FR', 'SA'];

				foreach (explode(',', $byDay) as $day)
				{
					if (in_array($day, $weekDays, true))
					{
						$days[] = $day;
					}
				}

				$byDay = $days;
			}

			if (empty($byDay))
			{
				return 0;
			}

			return mktime($h, $min, $length, $m, $d + round(($count - 1) / count($byDay) * 7 * $interval), $y);
		}

		return 0;
	}

	/**
	 * @param $PARENT_ID
	 * @param $userId
	 * @param $status
	 *
	 * @return void
	 *
	 * @throws \Bitrix\Calendar\Core\Base\BaseException
	 * @throws \Bitrix\Main\ArgumentException
	 * @throws \Bitrix\Main\SystemException
	 */
	private static function pushUpdateDescriptionToQueue($PARENT_ID, $userId, $status): void
	{
		$message = (new \Bitrix\Calendar\Core\Queue\Message\Message())
			->setBody([
				'parentId' => $PARENT_ID,
				'userId' => $userId,
				'meetingStatus' => $status,
			])
			->setRoutingKey('calendar:update_meeting_status');
		(new \Bitrix\Calendar\Core\Queue\Producer\Producer())->send($message);
	}

	public static function getEventPermissions(array $event, int $userId = 0)
	{
		if ($userId <= 0)
		{
			$userId = CCalendar::GetUserId();
		}
		$accessController = new EventAccessController($userId);
		$eventModel = self::getEventModelForPermissionCheck((int)($event['ID'] ?? 0), $event, $userId);

		$request = [
			ActionDictionary::ACTION_EVENT_VIEW_FULL => [],
			ActionDictionary::ACTION_EVENT_VIEW_TIME => [],
			ActionDictionary::ACTION_EVENT_VIEW_TITLE => [],
			ActionDictionary::ACTION_EVENT_VIEW_COMMENTS => [],
			ActionDictionary::ACTION_EVENT_EDIT => [],
			ActionDictionary::ACTION_EVENT_EDIT_LOCATION => [],
			ActionDictionary::ACTION_EVENT_EDIT_ATTENDEES => [],
			ActionDictionary::ACTION_EVENT_DELETE => [],
		];
		$accessResult = $accessController->batchCheck($request, $eventModel);

		return [
			'view_full' => $accessResult[ActionDictionary::ACTION_EVENT_VIEW_FULL],
			'view_time' => $accessResult[ActionDictionary::ACTION_EVENT_VIEW_TIME],
			'view_title' => $accessResult[ActionDictionary::ACTION_EVENT_VIEW_TITLE],
			'view_comments' => $accessResult[ActionDictionary::ACTION_EVENT_VIEW_COMMENTS],
			'edit' => $accessResult[ActionDictionary::ACTION_EVENT_EDIT],
			'editLocation' => $accessResult[ActionDictionary::ACTION_EVENT_EDIT_LOCATION],
			'editAttendees' => $accessResult[ActionDictionary::ACTION_EVENT_EDIT_ATTENDEES],
			'delete' => $accessResult[ActionDictionary::ACTION_EVENT_DELETE],
		];
	}

	public static function getEventModelForPermissionCheck(int $eventId, array $event = [], int $userId = 0): EventModel
	{
		if ($userId <= 0)
		{
			$userId = CCalendar::GetUserId();
		}

		$userEvent = false;
		if ($eventId > 0)
		{
			$userEventResult = self::GetList([
				'arFilter' => [
					'PARENT_ID' => $eventId,
					'OWNER_ID' => $userId,
					'CAL_TYPE' => [Dictionary::CALENDAR_TYPE['user'], Dictionary::CALENDAR_TYPE['open_event']],
					'DELETED' => 'N',
				],
				'arSelect' => self::$defaultSelectEvent,
				'parseRecursion' => false,
				'fetchMeetings' => false,
				'userId' => $userId,
				'checkPermissions' => false,
				'getPermissions' => false,
			]);

			if ($userEventResult)
			{
				$userEvent = $userEventResult[0];
			}
		}

		if (
			!$userEvent
			&& (
				empty($event)
				|| ((int)($event['ID'] ?? 0) !== $eventId)
			)
		)
		{
			$currentEventResult = self::GetList([
				'arFilter' => [
					'ID' => $eventId,
					'DELETED' => 'N',
				],
				'arSelect' => self::$defaultSelectEvent,
				'parseRecursion' => false,
				'fetchMeetings' => false,
				'userId' => $userId,
				'checkPermissions' => false,
				'getPermissions' => false,
			]);

			if ($currentEventResult)
			{
				$event = $currentEventResult[0];
			}
		}

		return EventModel::createFromArray($userEvent ?: $event ?: []);
	}

	public static function checkAttendeeBelongsToEvent($eventId, $userId)
	{
		if (empty($eventId) || empty($userId))
		{
			return false;
		}

		if (isset(self::$attendeeBelongingToEvent[$eventId][$userId]))
		{
			return self::$attendeeBelongingToEvent[$eventId][$userId];
		}

		$event = Internals\EventTable::query()
			->setSelect(['ID'])
			->where('PARENT_ID', $eventId)
			->where('OWNER_ID', $userId)
			->exec()->fetch()
		;

		if (!isset(self::$attendeeBelongingToEvent[$eventId]))
		{
			self::$attendeeBelongingToEvent[$eventId] = [];
		}
		if (!isset(self::$attendeeBelongingToEvent[$eventId][$userId]))
		{
			self::$attendeeBelongingToEvent[$eventId][$userId] = !empty($event);
		}

		return self::$attendeeBelongingToEvent[$eventId][$userId];
	}

	public static function getLimitDates(int $yearFrom, int $monthFrom, int $yearTo, int $monthTo): array
	{
		$userTimezoneName = \CCalendar::GetUserTimezoneName(\CCalendar::GetUserId());
		$offset = Util::getTimezoneOffsetUTC($userTimezoneName);

		return [
			'from' => \CCalendar::Date(mktime(0, 0, 0, $monthFrom, 1, $yearFrom) - $offset, false),
			'to' => \CCalendar::Date(mktime(0, 0, 0, $monthTo, 1, $yearTo) - $offset, false),
		];
	}

	private static function getOpenEventSection(?int $userId = null): ?Section
	{
		if (!(OpenEvents\Feature::getInstance()->isAvailable($userId)))
		{
			return null;
		}

		if (self::$openEventSection !== false)
		{
			return self::$openEventSection;
		}

		/** @var Factory $mapperFactory */
		$mapperFactory = ServiceLocator::getInstance()->get('calendar.service.mappers.factory');

		$openEventSection = $mapperFactory->getSection()->getMap([
			'=CAL_TYPE' => Dictionary::CALENDAR_TYPE['open_event'],
		])->fetch();

		self::$openEventSection = $openEventSection;

		return $openEventSection;
	}

	private static function getParentCollabConnections(array $eventList): array
	{
		$parentIds = [];
		foreach ($eventList as $event)
		{
			$collabEventTypes = [
				Dictionary::EVENT_TYPE['collab'],
				Dictionary::EVENT_TYPE['shared_collab'],
			];

			$isCollabEvent = (
				!empty($event['EVENT_TYPE'])
				&& in_array($event['EVENT_TYPE'], $collabEventTypes, true)
			);

			if (!$isCollabEvent || $event['ID'] === $event['PARENT_ID'])
			{
				continue;
			}

			$parentIds[$event['PARENT_ID']] = (int)$event['PARENT_ID'];
		}

		if (empty($parentIds))
		{
			return $parentIds;
		}

		$cachedCollabIdsByParent = array_intersect_key(self::$collabIdByParent, $parentIds);
		$cachedParentIds = array_keys($cachedCollabIdsByParent);

		$nonCachedParentIds = array_diff($parentIds, $cachedParentIds);

		if (empty($nonCachedParentIds))
		{
			return $cachedCollabIdsByParent;
		}

		$eventCollection =
			Internals\EventTable::query()
				->whereIn('ID', $nonCachedParentIds)
				->where('CAL_TYPE', Dictionary::CALENDAR_TYPE['group'])
				->setSelect(['ID', 'PARENT_ID', 'OWNER_ID'])
				->fetchCollection()
		;

		$nonCachedCollabIdsByParent = array_combine(
			$eventCollection->getParentIdList(),
			$eventCollection->getOwnerIdList(),
		);

		self::$collabIdByParent += $nonCachedCollabIdsByParent;

		return $cachedCollabIdsByParent + $nonCachedCollabIdsByParent;
	}

	private static function getCollabIdByEvent(array $event, array $parentCollabConnections): ?int
	{
		if (
			!($event['EVENT_TYPE'] ?? null)
			|| !in_array(
				$event['EVENT_TYPE'],
				[Dictionary::EVENT_TYPE['collab'], Dictionary::EVENT_TYPE['shared_collab']],
				true
			)
		)
		{
			return null;
		}

		$collabId = null;
		$parentId = (int)$event['PARENT_ID'];
		if ((int)$event['ID'] !== $parentId && !empty($parentCollabConnections[$parentId]))
		{
			$collabId = $parentCollabConnections[$parentId];
		}
		else if ($event['CAL_TYPE'] === Dictionary::CALENDAR_TYPE['group'])
		{
			$collabId = (int)$event['OWNER_ID'];
		}

		return $collabId;
	}

	/**
	 * @param int $parentId
	 *
	 * @return int
	 * @throws \Bitrix\Main\ArgumentException
	 * @throws \Bitrix\Main\ObjectPropertyException
	 * @throws \Bitrix\Main\SystemException
	 */
	private static function getCollabIdByParentId(int $parentId): int
	{
		if (isset(self::$collabIdByParent[$parentId]))
		{
			return self::$collabIdByParent[$parentId];
		}

		$result = Internals\EventTable::query()
			->setSelect(['ID', 'CAL_TYPE', 'OWNER_ID'])
			->where('CAL_TYPE', Dictionary::CALENDAR_TYPE['group'])
			->where('ID', $parentId)
			->setLimit(1)
			->exec()->fetch()
		;

		self::$collabIdByParent[$parentId] = (int)($result['OWNER_ID'] ?? 0);

		return self::$collabIdByParent[$parentId];
	}

	/**
	 * @param array $event
	 * @param int $sectionId
	 * @param int $userId
	 *
	 * @return array
	 * @throws \Bitrix\Main\Access\Exception\UnknownActionException
	 */
	private static function checkEventAccessFromGetList(array $event, int $sectionId, int $userId): array
	{
		if (isset(self::$getListAccessCheck[$sectionId]))
		{
			return self::$getListAccessCheck[$sectionId];
		}

		$eventModel = EventModel::createFromArray($event);
		$eventModel->setSectionId($sectionId);

		$accessController = new EventAccessController($userId);
		$request = [
			ActionDictionary::ACTION_EVENT_VIEW_FULL => [],
			ActionDictionary::ACTION_EVENT_VIEW_TIME => [],
			ActionDictionary::ACTION_EVENT_VIEW_TITLE => [],
		];

		self::$getListAccessCheck[$sectionId] = $accessController->batchCheck($request, $eventModel);

		return self::$getListAccessCheck[$sectionId];
	}

	private static function getAttendeeStatuses(
		$attendees,
		$fields,
		$params,
		$currentAttendeesIndex,
		$isNewEvent
	): array
	{
		$result = [];

		foreach ($attendees as $attendee)
		{
			$attendeeId = (int)$attendee;
			$meetingStatus = 'Q';
			$sendInvitations = false;

			if (
				(int)$fields['OWNER_ID'] === $attendeeId
				|| (int)$fields['CREATED_BY'] === $attendeeId
			)
			{
				$meetingStatus = 'H';
			}
			elseif ($isNewEvent && (int)($fields['~MEETING']['MEETING_CREATOR'] ?? null) === $attendeeId)
			{
				$meetingStatus = 'Y';
			}
			elseif (
				!empty($params['saveAttendeesStatus'])
				&& !empty($params['currentEvent']['ATTENDEE_LIST'])
				&& is_array($params['currentEvent']['ATTENDEE_LIST'])
			)
			{
				foreach($params['currentEvent']['ATTENDEE_LIST'] as $currentAttendee)
				{
					if ((int)$currentAttendee['id'] === $attendeeId)
					{
						$meetingStatus = $currentAttendee['status'];
						break;
					}
				}
			}

			if (
				!empty($currentAttendeesIndex[$attendeeId])
				&& $params['sendInvitesToDeclined']
				&& $meetingStatus === 'N'
			)
			{
				$meetingStatus = 'Q';
				$sendInvitations = true;
			}

			$result[$attendeeId] = [
				'id' => $attendeeId,
				'status' => $meetingStatus,
				'sendInvitations' => $sendInvitations
			];
		}

		return $result;
	}
}

Youez - 2016 - github.com/yon3zu
LinuXploit