<?php
namespace SabaiApps\Directories\Component\Booking;

use SabaiApps\Directories\Application;
use SabaiApps\Directories\Component\AbstractComponent;
use SabaiApps\Directories\Component\Entity;
use SabaiApps\Directories\Component\Field\IRenderers;
use SabaiApps\Directories\Component\Field\ITypes;
use SabaiApps\Directories\Component\Field\IWidgets;
use SabaiApps\Directories\Component\System;
use SabaiApps\Directories\Component\Display\ILabels;
use SabaiApps\Directories\Context;
use SabaiApps\Directories\Component\CSV;

class BookingComponent extends AbstractComponent implements
	Entity\IBundleTypes,
	ITypes,
	IWidgets,
	IRenderers,
	System\IMainRouter,
	ILabels,
	CSV\IImporters,
	CSV\IExporters,
	Entity\IFeatures,
	System\IScheduledActions {

	const VERSION = '2.0.6', PACKAGE = 'directories-pro';

	public static function description() {
		return 'Add booking features to content items.';
	}

	public static function interfaces() {
		return array( 'Dashboard\IPanels', 'Payment\IFeatures', 'Notification\IEvents' );
	}

	public function entityGetBundleTypeNames() {
		return array( 'booking_booking' );
	}

	public function entityGetBundleType( $name ) {
		return new EntityBundleType\BookingEntityBundleType( $this->_application, $name );
	}

	public function fieldGetTypeNames() {
		return array( 'booking_info', 'booking_status', 'booking_spaces' );
	}

	public function fieldGetType( $name ) {
		switch ( $name ) {
			case 'booking_info':
				return new FieldType\InfoFieldType( $this->_application, $name );
			case 'booking_status':
				return new FieldType\StatusFieldType( $this->_application, $name );
			case 'booking_spaces':
				return new FieldType\SpacesFieldType( $this->_application, $name );
		}
	}

	public function fieldGetWidgetNames() {
		return array( 'booking_info', 'booking_status', 'booking_spaces' );
	}

	public function fieldGetWidget( $name ) {
		switch ( $name ) {
			case 'booking_info':
				return new FieldWidget\InfoFieldWidget( $this->_application, $name );
			case 'booking_status':
				return new FieldWidget\StatusFieldWidget( $this->_application, $name );
			case 'booking_spaces':
				return new FieldWidget\SpacesFieldWidget( $this->_application, $name );
		}
	}

	public function fieldGetRendererNames() {
		return array( 'booking_date', 'booking_calendar', 'booking_spaces' );
	}

	public function fieldGetRenderer( $name ) {
		switch ( $name ) {
			case 'booking_date':
				return new FieldRenderer\DateFieldRenderer( $this->_application, $name );
			case 'booking_calendar':
				return new FieldRenderer\CalendarFieldRenderer( $this->_application, $name );
			case 'booking_spaces':
				return new FieldRenderer\SpacesFieldRenderer( $this->_application, $name );
		}
	}

	public function onDirectoryContentTypeSettingsFormFilter( &$form, $directoryType, $contentType, $info, $settings, $parents, $submitValues ) {
		if ( ! isset( $info['booking_enable'] )
			|| ! empty( $info['parent'] )
			|| ! empty( $info['is_taxonomy'] )
		) {
			return;
		}

		$form['booking_enable'] = array(
			'#type'          => 'checkbox',
			'#title'         => __( 'Enable bookings', 'directories-pro' ),
			'#default_value' => ! empty( $settings['booking_enable'] ) || is_null( $settings ),
			'#horizontal'    => true,
			'#weight'        => 70,
		);
	}

	public function onDirectoryContentTypeInfoFilter( &$info, $contentType, $settings = null ) {
		if ( ! isset( $info['booking_enable'] ) ) {
			return;
		}

		if ( ! empty( $info['is_taxonomy'] )
			|| ! empty( $info['parent'] )
			|| ! empty( $info['parent_field'] )
		) {
			unset( $info['booking_enable'] );
		}

		if ( ! isset( $settings['booking_enable'] )
			|| ! $settings['booking_enable']
		) {
			$info['booking_enable'] = false;
		}
	}

	public function onEntityBundlesInfoFilter( &$bundles, $group ) {
		foreach ( array_keys( $bundles ) as $bundle_type ) {
			$info =& $bundles[ $bundle_type ];

			if ( empty( $info['booking_enable'] )
				|| ! empty( $info['is_taxonomy'] )
				|| ! empty( $info['parent'] )
				|| ! empty( $info['parent_field'] )
			) {
				continue;
			}

			// Add booking bundle
			if ( ! isset( $bundles['booking_booking'] ) ) { // may already set if updating or importing
				$bundles['booking_booking'] = array();
			}
			$bundles['booking_booking']['parent']                        = $bundle_type; // must be bundle type for Entity component to create parent field
			$bundles['booking_booking']                                 += $this->entityGetBundleType( 'booking_booking' )->entityBundleTypeInfo();
			$bundles['booking_booking']['properties']['parent']['label'] = $info['label_singular'];

			return; // there should be only one bundle with booking enabled in a group
		}

		// No bundle with booking enabled found, so make sure the booking_booking bundle is not assigned
		unset( $bundles['booking_booking'] );
	}

	public function onEntityBundleInfoKeysFilter( &$keys ) {
		$keys[] = 'booking_enable';
		$keys[] = 'booking_number';
	}

	public function onEntitySaveEntityFields( $bundle, $entity, $isNew, &$fieldValues, &$updateProperties ) {
		if ( $bundle->type !== 'booking_booking' ) {
			return;
		}

		$updateProperties['title'] = $this->_application->Entity_NumberFormat_format(
			$entity,
			$bundle->info['label_singular'],
			isset( $bundle->info['booking_number'] ) ? $bundle->info['booking_number'] : array()
		);
	}

	public function systemMainRoutes( $lang = null ) {
		$routes = array();
		if ( $this->_application->isComponentLoaded( 'Dashboard' ) ) {
			// For dashboard
			$dashboard_slug = $this->_application->getComponent( 'Dashboard' )->getSlug( 'dashboard', $lang );
			$routes[ '/' . $dashboard_slug . '/:user_name/:panel_name/bookings' ]                           = array(
				'controller'    => 'Bookings',
				'callback_path' => 'bookings',
			);
			$routes[ '/' . $dashboard_slug . '/:user_name/:panel_name/bookings/:booking_id' ]               = array(
				'controller'      => 'ViewBooking',
				'callback_path'   => 'view_booking',
				'access_callback' => true,
				'format'          => array( ':booking_id' => '\d+' ),
			);
			$routes[ '/' . $dashboard_slug . '/:user_name/:panel_name/bookings/:booking_id/edit' ]          = array(
				'controller'      => 'EditBooking',
				'callback_path'   => 'edit_booking',
				'access_callback' => true,
			);
			$routes[ '/' . $dashboard_slug . '/:user_name/:panel_name/bookings/:booking_id/cancel' ]        = array(
				'controller'      => 'CancelBooking',
				'callback_path'   => 'cancel_booking',
				'access_callback' => true,
			);
			$routes[ '/' . $dashboard_slug . '/:user_name/:panel_name/bookings_received' ]                  = array(
				'controller'    => 'ReceivedBookings',
				'callback_path' => 'bookings_received',
			);
			$routes[ '/' . $dashboard_slug . '/:user_name/:panel_name/bookings/:booking_id/change_status' ] = array(
				'controller'      => 'ChangeBookingStatus',
				'callback_path'   => 'change_status',
				'access_callback' => true,
			);
		}

		return $routes;
	}

	public function systemOnAccessMainRoute( Context $context, $path, $accessType, array &$route ) {
		switch ( $path ) {
			case 'view_booking':
				if ( $accessType === Application::ROUTE_ACCESS_LINK ) {
					if ( ( ! $booking_id = $context->getRequest()->asInt( 'booking_id' ) )
						|| ( ! $booking = $this->_application->Entity_Entity( 'post', $booking_id ) )
						|| $booking->getBundleType() !== 'booking_booking'
					) {
						$context->setNotFoundError();
						return false;
					}
					if ( ! $this->_application->Entity_IsAuthor( $booking ) ) {
						// Not booker
						if ( ( ! $parent_entity = $this->_application->Entity_ParentEntity( $booking, false ) )
							|| ! $this->_application->Entity_IsAuthor( $parent_entity )
						) {
							// Not bookie
							return false;
						}
					}
					$context->booking = $booking;
				}
				return true;
			case 'edit_booking':
			case 'cancel_booking':
				if ( $accessType === Application::ROUTE_ACCESS_LINK ) {
					if ( ! $this->_application->Entity_IsAuthor( $context->booking ) ) {
						return false; // not booker
					}
					if ( $path === 'cancel_booking' ) {
						if ( ! $this->_application->Booking_Util_canChangeStatus( $context->booking, 'cancelled' ) ) {
							return false;
						}
					}
				}
				return true;
			case 'change_status':
				if ( $accessType === Application::ROUTE_ACCESS_LINK ) {
					if ( ( ! $status = $context->getRequest()->asStr( 'status' ) )
						|| ! $this->_application->Booking_Util_canChangeStatus( $context->booking, $status, 'bookie' )
					) {
						return false;
					}

					$context->booking_status = $status;
				}
				return true;
		}
	}

	public function systemMainRouteTitle( Context $context, $path, $titleType, array $route ) {}

	public function displayGetLabelNames( Entity\Model\Bundle $bundle ) {
		return array( 'booking_status' );
	}

	public function displayGetLabel( $name ) {
		return new DisplayLabel\StatusDisplayLabel( $this->_application, $name );
	}

	public function notificationGetEventNames() {
		return array( 'booking_confirmed', 'booking_cancelled', 'booking_pending', 'booking_rejected', 'booking_reminder', 'booking_system_cancelled' );
	}

	public function notificationGetEvent( $name ) {
		switch ( $name ) {
			case 'booking_confirmed':
				return new NotificationEvent\BookingConfirmedNotificationEvent( $this->_application, $name );
			case 'booking_cancelled':
				return new NotificationEvent\BookingCancelledNotificationEvent( $this->_application, $name );
			case 'booking_pending':
				return new NotificationEvent\BookingPendingNotificationEvent( $this->_application, $name );
			case 'booking_rejected':
				return new NotificationEvent\BookingRejectedNotificationEvent( $this->_application, $name );
			case 'booking_reminder':
				return new NotificationEvent\BookingReminderNotificationEvent( $this->_application, $name );
			case 'booking_system_cancelled':
				return new NotificationEvent\BookingSystemCancelledNotificationEvent( $this->_application, $name );
		}
	}

	public function dashboardGetPanelNames() {
		return array( 'booking_bookings' );
	}

	public function dashboardGetPanel( $name ) {
		return new DashboardPanel\BookingsDashboardPanel( $this->_application, $name );
	}

	public function onDashboardPostsPanelDefaultSortsFilter( &$sorts, $bundle ) {
		if ( $bundle->type === 'booking_booking' ) {
			$sorts = array(
				'booking_date',
				'booking_date,desc',
			);
		}
	}

	public function onEntityCreateEntitySuccess( $bundle, $entity, $values, $extraArgs ) {
		if ( $bundle->type !== 'booking_booking'
			|| ( ! $parent_entity = $this->_application->Entity_ParentEntity( $entity, false ) )
		) {
			return;
		}

		if ( ! $status = $entity->getSingleFieldValue( 'booking_status' ) ) {
			$status = 'pending';
		}
		$this->_application->Action( 'booking_booking_' . $status, array( $entity, $parent_entity, null ) );
		$this->_application->Action( 'booking_booking_status_change', array( $entity, null ) );
	}

	public function onEntityUpdateEntitySuccess( $bundle, $entity, $oldEntity, $values, $extraArgs ) {
		if ( $bundle->type !== 'booking_booking'
			|| empty( $values['booking_status'] )
			|| 'pending' === ( $status = $entity->getSingleFieldValue( 'booking_status' ) ) // make sure not pending
			|| ( ! $parent_entity = $this->_application->Entity_ParentEntity( $entity, false ) )
		) {
			return;
		}

		$this->_application->Action( 'booking_booking_' . $status, array( $entity, $parent_entity, $oldEntity ) );

		if ( $oldEntity->getSingleFieldValue( 'booking_status' ) !== $status ) {
			$this->_application->Action( 'booking_booking_status_change', array( $entity, $oldEntity->getSingleFieldValue( 'booking_status' ) ?? 'pending' ) );
		}
	}

	public function systemScheduledActions() {
		if ( ! $this->_application->Entity_Bundles_byFeature( 'booking_enable' ) ) {
			return array();
		}

		return array(
			'booking_upcoming_bookings' => array(
				'title'       => __( 'Send Upcoming Booking Reminders', 'directories-pro' ),
				'description' => __( 'Sends upcoming booking reminders to users.', 'directories-pro' ),
				'recurrence'  => 'daily',
				'args'        => array(),
				'priority'    => 10,
			),
		);
	}

	public function onBookingUpcomingBookings() {
		$upcoming_days = 7;
		$current_ts    = time();
		$bookings      = $this->_application->Entity_Query( 'post' )
			->fieldIs( 'bundle_type', 'booking_booking' )
			->fieldIs( 'booking_status', 'confirmed' )
			->fieldIsSmallerThan( 'booking_info', ( $current_ts - ( $current_ts % 86400 ) ) + 86400 * $upcoming_days, 'start_date' )
			->fieldIsGreaterThan( 'booking_info', $current_ts, 'start_date' );
		foreach ( $bookings->fetch() as $booking ) {
			$this->_application->getPlatform()->getScheduler()->enqueueAction( 'booking_upcoming', array( $booking->getId() ) );
		}
	}

	public function csvGetImporterNames() {
		return array( 'booking_info' );
	}

	public function csvGetImporter( $name ) {
		return new CSVImporter\BookingCSVImporter( $this->_application, $name );
	}

	public function csvGetExporterNames() {
		return array( 'booking_info' );
	}

	public function csvGetExporter( $name ) {
		return new CSVExporter\BookingCSVExporter( $this->_application, $name );
	}

	public function onEntityPermissionsFilter( &$permissions, Entity\Model\Bundle $bundle ) {
		// Remove unused permissions
		if ( $bundle->type === 'booking_booking' ) {
			unset( $permissions['entity_edit'], $permissions['entity_edit_others'], $permissions['entity_delete'], $permissions['entity_delete_others'] );
		}
	}

	public function isBookingsEnabled( $entity ) {
		if ( ( ! $bundle = $this->_application->Entity_Bundle( $entity ) )
			|| empty( $bundle->info['booking_enable'] )
		) {
			return false;
		}

		return $this->_application->Entity_Levels_hasFeature( $entity, 'booking_bookings' );
	}

	public function entityGetFeatureNames() {
		return array( 'booking_bookings' );
	}

	public function entityGetFeature( $name ) {
		return new EntityFeature\BookingsEntityFeature( $this->_application, $name );
	}

	public function paymentGetFeatureNames() {
		return array( 'booking_bookings' );
	}

	public function paymentGetFeature( $name ) {
		return new PaymentFeature\BookingsPaymentFeature( $this->_application, $name );
	}

	public function onCalendarOccurrenceContentFilter( &$content, $occurrence, $entity, $settings, array &$options ) {
		if ( empty( $settings['booking_enable'] ) ) {
			return;
		}

		if ( ! empty( $settings['booking_allowed_only'] )
			&& empty( $occurrence->slot_data_snapshot['booking_allow'] )
		) {
			$content = false; // hides this occurrence
		}

		if ( ( $bundle = $this->_application->Entity_Bundle( $occurrence->bundle_name ) )
			&& ! empty( $bundle->info['booking_enable'] )
			&& ( $booking_bundle = $this->_application->Entity_Bundle( 'booking_booking', $bundle->group ) )
		) {
			if ( isset( $occurrence->data['booking_spaces_available'] )
				&& $occurrence->data['booking_spaces_available'] <= 0
			) {
				$options['color'] = 'var(--bs-danger)';
			} else {
				$content = (string) $this->_application->Url(
					'/' . $this->_application->FrontendSubmit_AddEntitySlug( $booking_bundle ),
					array(
						'entity_id'       => $entity->getId(),
						'occurrence_hash' => $occurrence->hash,
					),
					'',
					'&'
				);
				// Show in modal only when user is logged in, otherwise show as link for the redirection to work properly
				if ( ! $this->_application->getUser()->isAnonymous() ) {
					$options['modal']       = true;
					$options['modal_title'] = $booking_bundle->getLabel( 'add' ) . ' - ' . $occurrence->title;
				}
			}
		}
	}

	public function onBookingBookingStatusChange( $booking, $oldStatus ) {
		$new_status = $booking->getSingleFieldValue( 'booking_status' ) ?? 'pending';
		if ( ! $oldStatus
			&& $new_status === 'pending'
		) {
			$this->_application->Booking_Util_updateOccurrenceAvailableSpaces( $booking, true );
			return;
		}
		switch ( $oldStatus ) {
			case 'pending':
				switch ( $new_status ) {
					case 'failed':
					case 'rejected':
					case 'cancelled':
					case 'system_cancelled':
						$this->_application->Booking_Util_updateOccurrenceAvailableSpaces( $booking );
						break;
				}
				break;
			case 'confirmed':
				switch ( $new_status ) {
					case 'cancelled':
					case 'system_cancelled':
						$this->_application->Booking_Util_updateOccurrenceAvailableSpaces( $booking );
						break;
				}
				break;
		}
	}

	public function onCalendarOccurrencesChanged( $bundle, $entity, $occurrences ) {
		$this->_cancelBookingsByOccurrences( $bundle, $occurrences );
	}

	public function onCalendarOccurrencesDeleted( $bundle, $entity, $occurrences ) {
		$this->_cancelBookingsByOccurrences( $bundle, $occurrences );
	}

	protected function _cancelBookingsByOccurrences( $bundle, array $occurrences ) {
		if ( ! $booking_bundle = $this->_application->Entity_Bundle( 'booking_booking', $bundle->group ) ) {
			return;
		}

		$bookings = $this->_application->Entity_Query( 'post', $booking_bundle->name )
			->fieldIsIn( 'booking_info', array_keys( $occurrences ), 'occurrence_hash' )
			->startCriteriaGroup( 'OR' )
				->fieldIsIn( 'booking_status', array( 'confirmed', 'pending' ) )
				->fieldIsNull( 'booking_status' )
			->finishCriteriaGroup()
			->fetch();
		foreach ( $bookings as $booking ) {
			$current_info                    = $booking->getSingleFieldValue( 'booking_info' );
			$current_hash                    = $current_info['occurrence_hash'];
			$occurrence                      = $occurrences[ $current_hash ];
			$current_info['occurrence_hash'] = $occurrence->hash;
			$values                          = array(
				'booking_status' => 'system_cancelled',
				'booking_info'   => $current_info,
			);
			$booking                         = $this->_application->Entity_Save( $booking, $values );
			$this->_application->logDebug(
				sprintf(
					'Booking %s cancelled due to occurrence %s being cancelled.',
					$booking->getId(),
					$current_hash
				)
			);
		}
	}
}
