<?php

namespace MotoPress\Appointment\Handlers;

use MotoPress\Appointment\Entities\Booking;
use MotoPress\Appointment\Entities\Reservation;
use MotoPress\Appointment\Helpers\ReservationHelper;
use MotoPress\Appointment\PostTypes\BookingPostType;
use MotoPress\Appointment\Helpers\AvailabilityHelper;
use MotoPress\Appointment\PostTypes\ReservationPostType;
use MotoPress\Appointment\PostTypes\Statuses\BookingStatuses;

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * This handler uses https://github.com/googleapis/google-api-php-client/tree/v2.1.3
 * @since 1.10.0
 */
class GoogleCalendarSyncHandler {

	const MPA_EMPLOYEE_META_KEY_GOOGLE_CALENDAR_TOKEN       = 'mpa_google_calendar_token';
	const MPA_RESERVATION_META_KEY_GOOGLE_CALENDAR_EVENT_ID = 'mpa_google_calendar_event_id';

	/**
	 * @since 1.10.0
	 */
	public function __construct() {
		if ( empty( mpapp()->settings()->getGoogleCalendarClientId() ) ||
			empty( mpapp()->settings()->getGoogleCalendarClientSecret() ) ) {
			return;
		}

		add_action(
			'init',
			function() {

				if ( false !== strpos( $_SERVER['REQUEST_URI'], '/mpa-google-calendar-sync/' ) ) {
					$this->saveGoogleCalendarAccessToken();
				}
			}
		);

		add_action(
			'mpa_booking_status_changed',
			function( $mpaBooking, $newStatus, $oldStatus ) {

				if ( 'confirmed' === $newStatus ) {

					$this->syncBookingToGoogleCalendar( $mpaBooking );
				}
			},
			10,
			3
		);

		add_action(
			'mpa_booking_placed_by_user',
			function( $mpaBooking ) {

				$this->syncBookingToGoogleCalendar( $mpaBooking );
			},
			10,
			1
		);

		add_action(
			'mpa_booking_placed_by_admin',
			function( $mpaBooking ) {

				$this->syncBookingToGoogleCalendar( $mpaBooking );
			},
			10,
			1
		);

		add_action(
			'wp_trash_post',
			function( int $postId ) {

				$post = get_post( $postId );

				if ( BookingPostType::POST_TYPE == $post->post_type ) {

					$mpaBooking = mpapp()->repositories()->booking()->mapPostToEntity( $post );

					foreach ( $mpaBooking->getReservations() as $reservation ) {

						$this->deleteReservationFromGoogleCalendar( $reservation );
					}
				}
			},
			10,
			2
		);

		add_action(
			'mpa_booking_pending',
			function( $mpaBooking, $oldStatus, $isNewPost ) {

				if ( $isNewPost ) {
					return;
				}

				foreach ( $mpaBooking->getReservations() as $reservation ) {

					$this->deleteReservationFromGoogleCalendar( $reservation );
				}
			},
			10,
			3
		);

		add_action(
			'mpa_booking_cancelled',
			function( $mpaBooking ) {

				foreach ( $mpaBooking->getReservations() as $reservation ) {

					$this->deleteReservationFromGoogleCalendar( $reservation );
				}
			},
			10,
			1
		);

		add_action(
			'before_delete_post',
			function( $postId, $post ) {

				if ( ReservationPostType::POST_TYPE == $post->post_type ) {
					$reservation = mpapp()->repositories()->reservation()->mapPostToEntity( $post );
					$this->deleteReservationFromGoogleCalendar( $reservation );
				}
			},
			10,
			2
		);

		add_action(
			'mpa_reservations_updated',
			function( $mpaBooking ) {
				$this->syncBookingToGoogleCalendar( $mpaBooking );
			},
			10,
			1
		);
	}

	/**
	 * @since 1.10.0
	 */
	public static function isGoogleCalendarConnectedToEmployee( int $mpaEmployeeId ): bool {

		$token  = static::getStoredGoogleAPIToken( $mpaEmployeeId );
		$client = static::getGoogleAPIClient( $mpaEmployeeId );

		$isConnectedToEmploye = ! empty( $token ) && ! $client->isAccessTokenExpired();
		return apply_filters( 'mpa_is_google_calendar_connected_to_employee', $isConnectedToEmploye );
	}

	/**
	 * @since 1.10.0
	 */
	public static function disconnectGoogleCalendarFromEmployee( int $mpaEmployeeId ) {
		delete_post_meta(
			$mpaEmployeeId,
			static::MPA_EMPLOYEE_META_KEY_GOOGLE_CALENDAR_TOKEN
		);
	}

	/**
	 * @since 1.10.0
	 */
	private static function getStoredGoogleAPIToken( int $mpaEmployeeId ): array {
		$googleAPIToken = get_post_meta( $mpaEmployeeId, static::MPA_EMPLOYEE_META_KEY_GOOGLE_CALENDAR_TOKEN, true );
		return ! empty( $googleAPIToken ) ? $googleAPIToken : array();
	}

	/**
	 * @since 1.10.0
	 */
	public static function getGoogleCalendarConnectionURL( int $mpaEmployeeId ): string {
		$googleAPI = static::getGoogleAPIClient( $mpaEmployeeId );
		$authUrl   = $googleAPI->createAuthUrl();
		return $authUrl;
	}

	/**
	 * Doc about authorization https://developers.google.com/identity/protocols/oauth2/web-server
	 * @since 1.10.0
	 */
	private static function getGoogleAPIClient( int $mpaEmployeeId ) {
		$googleAPI = new \Google_Client();
		$googleAPI->setScopes( \Google_Service_Calendar::CALENDAR );
		$googleAPI->setAuthConfig(
			array(
				'client_id'     => mpapp()->settings()->getGoogleCalendarClientId(),
				'client_secret' => mpapp()->settings()->getGoogleCalendarClientSecret(),
				'redirect_uris' => array( get_site_url() . '/mpa-google-calendar-sync/' ),
			)
		);
		$googleAPI->setAccessType( 'offline' );
		$googleAPI->setPrompt( 'select_account consent' );
		// if we will need more parameters then create array, json_encode() it and base64UrlEncode() for safe url
		$googleAPI->setState( $mpaEmployeeId );

		$accessToken = static::getStoredGoogleAPIToken( $mpaEmployeeId );
		if ( ! empty( $accessToken ) ) {

			$googleAPI->setAccessToken( $accessToken );
		}

		$googleAPI->setTokenCallback(
			function( $cacheKey, $accessToken ) use ( $mpaEmployeeId ) {

				update_post_meta(
					$mpaEmployeeId,
					static::MPA_EMPLOYEE_META_KEY_GOOGLE_CALENDAR_TOKEN,
					$accessToken
				);
			}
		);

		// If there is no previous token or it's expired.
		if ( $googleAPI->isAccessTokenExpired() ) {
			//Refresh the token if possible, else fetch a new one.
			if ( $googleAPI->getRefreshToken() ) {

				$googleAPI->fetchAccessTokenWithRefreshToken( $googleAPI->getRefreshToken() );
			}
		}

		return $googleAPI;
	}

	/**
	 * @since 1.10.0
	 */
	private function saveGoogleCalendarAccessToken() {
		$mpaEmployeeId = empty( $_GET['state'] ) ? 0 : absint( $_GET['state'] );
		$mpaEmployee   = mpapp()->repositories()->employee()->findById( $mpaEmployeeId );

		$googleAPIAuthCode = sanitize_text_field( $_GET['code'] );

		if ( empty( $mpaEmployee ) || empty( $googleAPIAuthCode ) ) {

			// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
			error_log(
				'Appointment Booking error: Could not get Google Calendar Access Token [ Employee id = ' . $mpaEmployeeId .
				', code = ' . $googleAPIAuthCode
			);
			return;
		}

		$googleAPI = static::getGoogleAPIClient( $mpaEmployeeId );

		// Exchange authorization code for an access token.
		$accessToken = $googleAPI->fetchAccessTokenWithAuthCode( $googleAPIAuthCode );
		$googleAPI->setAccessToken( $accessToken );

		// Check to see if there was an error.
		if ( array_key_exists( 'error', $accessToken ) ) {
			// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
			error_log( 'Appointment Booking error: Could not get Google Calendar Access Token - ' . $accessToken );
		}

		update_post_meta(
			$mpaEmployeeId,
			static::MPA_EMPLOYEE_META_KEY_GOOGLE_CALENDAR_TOKEN,
			$googleAPI->getAccessToken()
		);

		wp_safe_redirect( get_edit_post_link( $mpaEmployeeId, '' ) );
		exit;
	}

	/**
	 * Doc about Google calendar https://developers.google.com/calendar/api/guides/create-events#php
	 * @since 1.10.0
	 */
	private function addNewReservationToGoogleCalendar( Booking $mpaBooking, Reservation $mpaReservation ) {

		do_action( 'mpa_google_calendar_reservation_before_add', $mpaReservation );

		if ( ! static::isGoogleCalendarConnectedToEmployee( $mpaReservation->getEmployeeId() ) ) {
			return;
		}

		$googleAPI = apply_filters( 'mpa_google_calendar_google_api_client', static::getGoogleAPIClient( $mpaReservation->getEmployeeId() ) );

		$calendar  = new \Google_Service_Calendar( $googleAPI );

		// Modify optParams as needed; for example, you can add 'conferenceDataVersion'
		$optParams = apply_filters( 'mpa_google_calendar_insert_opt_params', array() );

		try {
			$event = $calendar->events->insert(
				'primary',
				new \Google_Service_Calendar_Event( $this->collectGoogleCalendarEventData( $mpaBooking, $mpaReservation ) ),
				$optParams
			);

			do_action( 'mpa_google_calendar_after_event_insert', $event, $mpaReservation );

			update_post_meta(
				$mpaReservation->getId(),
				static::MPA_RESERVATION_META_KEY_GOOGLE_CALENDAR_EVENT_ID,
				$event->getId()
			);
		} catch ( \Throwable $e ) {
			do_action( 'mpa_google_calendar_event_add_error', $e->getMessage(), $mpaBooking );
			// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
			error_log(
				'### Appointment Booking Error: ' . $e->getMessage() . PHP_EOL .
				$e->getTraceAsString()
			);
		}
	}

	/**
	 * @since 1.10.0
	 */
	private function collectGoogleCalendarEventData( Booking $mpaBooking, Reservation $mpaReservation ) {

		$timeZoneFromSettings = wp_timezone();
		$eventStart           = $mpaReservation->getServiceTime()->getStartTime();
		$eventStart           = new \DateTime( $eventStart->format( 'Y-m-d H:i:s' ), $timeZoneFromSettings );

		$eventEnd = $mpaReservation->getServiceTime()->getEndTime();
		$eventEnd = new \DateTime( $eventEnd->format( 'Y-m-d H:i:s' ), $timeZoneFromSettings );

		$serviceName  = get_the_title( $mpaReservation->getServiceId() );
		$employeeName = get_the_title( $mpaReservation->getEmployeeId() );

		$eventData = array(
			'location' => get_the_title( $mpaReservation->getLocationId() ),
			'start'    => array(
				// format like 2016-07-04T17:53:30+02:00
				'dateTime' => $eventStart->format( \DateTime::RFC3339 ),
			),
			'end'      => array(
				// format like 2016-07-04T17:53:30+02:00
				'dateTime' => $eventEnd->format( \DateTime::RFC3339 ),
			),
		);

		// generate summary and description on group or default serevations
		if ( $mpaReservation->getService()->isGroupService() ) {

			$eventData['description'] = $this->generateDesriptionFromGroupReservedSlots( $mpaReservation );

			$eventData['summary'] = $serviceName . ' / ' .
				$mpaReservation->getService()->getQuantityLabel() . ': ' .
				$this->calcGroupTotalReservedCapacity( $mpaReservation ) . ' / ' .
				$employeeName . ' / ' .
				str_replace( 'https://', '', get_site_url( null, '', 'https' ) );

		} else {

			$eventData['description'] = esc_html__( 'Booking', 'motopress-appointment' ) . ' #' .
				$mpaBooking->getId() . ': ' .
				$serviceName . ', ' .
				$mpaBooking->getCustomerName() . ' ' .
				$mpaBooking->getCustomerPhone() . ' ' .
				$mpaBooking->getCustomerEmail();

			$eventData['summary'] = $serviceName . ' / ' .
				$mpaReservation->getService()->getQuantityLabel() . ': ' .
				$mpaReservation->getCapacity() . ' / ' .
				$employeeName . ' / ' .
				str_replace( 'https://', '', get_site_url( null, '', 'https' ) );
		}

		$eventData = apply_filters( 'mpa_google_calendar_pre_collect_event_data', $eventData );

		return $eventData;
	}

	/**
	 * @since 1.10.0
	 */
	private function updateReservationInGoogleCalendar( Booking $mpaBooking, Reservation $mpaReservation ) {

		do_action( 'mpa_google_calendar_reservation_before_update', $mpaReservation );

		if ( ! static::isGoogleCalendarConnectedToEmployee( $mpaReservation->getEmployeeId() ) ) {
			return;
		}

		$googleAPI = apply_filters( 'mpa_google_calendar_google_api_client', static::getGoogleAPIClient( $mpaReservation->getEmployeeId() ) );
		$calendar  = new \Google_Service_Calendar( $googleAPI );

		$groupReservedEventId = $this->getEventIdFromGroupReservedSlots( $mpaReservation );
		if ( $mpaReservation->getService()->isGroupService() && ! empty( $groupReservedEventId ) ) {
			// update event id for group reservations
			update_post_meta(
				$mpaReservation->getId(),
				static::MPA_RESERVATION_META_KEY_GOOGLE_CALENDAR_EVENT_ID,
				$groupReservedEventId
			);

			do_action( 'mpa_google_calendar_updated_group_reservation_event_id', $mpaReservation );

			$eventId = $groupReservedEventId;

		} else {

			$eventId = get_post_meta(
				$mpaReservation->getId(),
				static::MPA_RESERVATION_META_KEY_GOOGLE_CALENDAR_EVENT_ID,
				true
			);
		}

		try {

			$event = $calendar->events->update(
				'primary',
				$eventId,
				new \Google_Service_Calendar_Event( $this->collectGoogleCalendarEventData( $mpaBooking, $mpaReservation ) )
			);

		} catch ( \Throwable $e ) {
			do_action( 'mpa_google_calendar_event_update_error', $e->getMessage(), $mpaBooking );
			// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
			error_log(
				'### Appointment Booking Error: ' . $e->getMessage() . PHP_EOL .
				$e->getTraceAsString()
			);
		}
	}

	/**
	 * @since 1.10.0
	 */
	private function deleteReservationFromGoogleCalendar( Reservation $mpaReservation ) {

		do_action( 'mpa_google_calendar_reservation_before_delete', $mpaReservation );

		if ( ! static::isGoogleCalendarConnectedToEmployee( $mpaReservation->getEmployeeId() ) ) {
			return;
		}

		$groupReservedEventId = $this->getEventIdFromGroupReservedSlots( $mpaReservation );
		if ( $mpaReservation->getService()->isGroupService() && ! empty( $groupReservedEventId ) ) {
			$this->updateReservationInGoogleCalendar( $mpaReservation->getBooking(), $mpaReservation );
			return;
		}

		$googleAPI = apply_filters( 'mpa_google_calendar_google_api_client', static::getGoogleAPIClient( $mpaReservation->getEmployeeId() ) );
		$calendar  = new \Google_Service_Calendar( $googleAPI );

		$eventId = get_post_meta(
			$mpaReservation->getId(),
			static::MPA_RESERVATION_META_KEY_GOOGLE_CALENDAR_EVENT_ID,
			true
		);

		if ( ! empty( $eventId ) ) {
			try {

				$result = $calendar->events->delete( 'primary', $eventId );

				if ( 204 == $result->getStatusCode() ) {

					update_post_meta(
						$mpaReservation->getId(),
						static::MPA_RESERVATION_META_KEY_GOOGLE_CALENDAR_EVENT_ID,
						null
					);
				}
			} catch ( \Throwable $e ) {
				// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
				error_log(
					'### Appointment Booking Error: ' . $e->getMessage() . PHP_EOL .
					$e->getTraceAsString()
				);
			}
		}
	}

	/**
	 * @since 1.10.0
	 */
	private function syncBookingToGoogleCalendar( Booking $mpaBooking ) {
		if ( BookingStatuses::STATUS_CANCELLED == $mpaBooking->getStatus() ||
			BookingStatuses::STATUS_PENDING == $mpaBooking->getStatus() ) {
			return;
		}

		foreach ( $mpaBooking->getReservations() as $reservation ) {

			$eventId = get_post_meta(
				$reservation->getId(),
				static::MPA_RESERVATION_META_KEY_GOOGLE_CALENDAR_EVENT_ID,
				true
			);

			if ( ! empty( $eventId ) ||
				$reservation->getService()->isGroupService() && ! empty( $this->getEventIdFromGroupReservedSlots( $reservation ) ) ) {

				$this->updateReservationInGoogleCalendar( $mpaBooking, $reservation );
			} else {
				$this->addNewReservationToGoogleCalendar( $mpaBooking, $reservation );
			}
		}
	}

	/**
	 * Get event id for group reserved slots
	 *
	 * @param Reservation $groupReservation
	 * @return string
	 */
	public static function getEventIdFromGroupReservedSlots( $groupReservation ): string {
		$reservedSlots = ReservationHelper::findReservedSlotsByReservation( $groupReservation );

		$reservedSlots = ReservationHelper::filterNonIntersectingPeriods(
			$reservedSlots,
			$groupReservation->getBufferTime()
		);

		foreach ( $reservedSlots as $reservedSlot ) {
			$reservedGoogleCalendarEventId = get_post_meta( $reservedSlot->getId(), static::MPA_RESERVATION_META_KEY_GOOGLE_CALENDAR_EVENT_ID, true );

			if ( ! empty( $reservedGoogleCalendarEventId ) ) {
				return $reservedGoogleCalendarEventId;
			}
		}
		return '';
	}

	/**
	 * Generate description for group reservations
	 *
	 * @param Reservation $groupReservation
	 * @return string
	 */
	public static function generateDesriptionFromGroupReservedSlots( $groupReservation ): string {
		$reservedSlots = ReservationHelper::findReservedSlotsByReservation( $groupReservation );
		$reservedSlots = ReservationHelper::filterNonIntersectingPeriods(
			$reservedSlots,
			$groupReservation->getBufferTime()
		);

		$triggeredBookingConfirmationOrChange = did_action( 'mpa_booking_confirmed' ) > 0 || did_action( 'mpa_booking_status_changed' ) > 0;
		$confirmationModeIsNotAuto            = 'auto' !== mpapp()->settings()->getConfirmationMode();

		$isNotPendingOrCancelled = function ( $booking ) {
			return BookingStatuses::STATUS_PENDING !== $booking->getStatus() &&
					BookingStatuses::STATUS_CANCELLED !== $booking->getStatus();
		};

		$noRemoveActionsTriggered = (
			did_action( 'wp_trash_post' ) === 0 &&
			did_action( 'mpa_booking_pending' ) === 0 &&
			did_action( 'mpa_booking_cancelled' ) === 0 &&
			did_action( 'before_delete_post' ) === 0
		);

		if ( $confirmationModeIsNotAuto && $triggeredBookingConfirmationOrChange ) {
			$mpaBooking = $groupReservation->getBooking();

			if ( $isNotPendingOrCancelled( $mpaBooking ) ) {
				array_push( $reservedSlots, $groupReservation );
			}
		} elseif ( $noRemoveActionsTriggered ) {
			array_push( $reservedSlots, $groupReservation );
		}

		$descr = '';
		foreach ( $reservedSlots as $reservedSlot ) {
			$booking   = $reservedSlot->getBooking( true );
			$bookingId = $booking->getId();
			$descr    .= esc_html__( 'Booking', 'motopress-appointment' ) . ' #' .
				$bookingId . ', ' .
				$booking->getCustomerName() . ' ' .
				$booking->getCustomerPhone() . ' ' .
				$booking->getCustomerEmail() . ' ' .
				esc_html__( 'Capacity: ', 'motopress-appointment' ) .
				$reservedSlot->getCapacity() . '<br><br>';
		}

		return $descr;
	}

	/**
	 * Calc capacity for group reservations
	 *
	 * @param Reservation $mpaReservation
	 * @return integer
	 */
	public static function calcGroupTotalReservedCapacity( $mpaReservation ): int {
		$reservedSlots = ReservationHelper::findReservedSlotsByReservation( $mpaReservation );
		$reservedSlots = ReservationHelper::filterNonIntersectingPeriods(
			$reservedSlots,
			$mpaReservation->getBufferTime()
		);
		$freeCapacity  = AvailabilityHelper::calcFreeCapacityForSlot( $mpaReservation, $reservedSlots );

		$employeeId  = $mpaReservation->getEmployeeId();
		$maxCapacity = $mpaReservation->getService()->getMaxCapacity( $employeeId );

		$triggeredBookingConfirmationOrChange = did_action( 'mpa_booking_confirmed' ) > 0 || did_action( 'mpa_booking_status_changed' ) > 0;
		$confirmationModeIsNotAuto            = 'auto' !== mpapp()->settings()->getConfirmationMode();
		$removeActionsTriggered               = (
			did_action( 'wp_trash_post' ) > 0 ||
			did_action( 'mpa_booking_pending' ) > 0 ||
			did_action( 'mpa_booking_cancelled' ) > 0 ||
			did_action( 'before_delete_post' ) > 0
		);

		if ( $confirmationModeIsNotAuto && $triggeredBookingConfirmationOrChange ) {
			$mpaBooking = $mpaReservation->getBooking();
			if (
				BookingStatuses::STATUS_PENDING !== $mpaBooking->getStatus() &&
				BookingStatuses::STATUS_CANCELLED !== $mpaBooking->getStatus()
			) {
				return $maxCapacity - $freeCapacity + $mpaReservation->getCapacity();
			}
		}

		if ( $removeActionsTriggered ) {
			return $maxCapacity - $freeCapacity;
		}

		return $maxCapacity - $freeCapacity + $mpaReservation->getCapacity();
	}
}
