<?php
namespace BooklyOutlookCalendar\Lib\Microsoft;

use Bookly\Lib\Config;
use Bookly\Lib\Entities\Appointment;
use Bookly\Lib\Entities\Customer;
use Bookly\Lib\Entities\CustomerAppointment;
use Bookly\Lib\Entities\Payment;
use Bookly\Lib\Entities\Service;
use Bookly\Lib\Entities\Staff;
use Bookly\Lib\Entities\StaffService;
use Bookly\Lib\DataHolders\Booking\Order;
use Bookly\Lib\DataHolders\Booking\Simple;
use Bookly\Lib\Notifications\Assets\Item\Codes;
use Bookly\Lib\Slots\DatePoint;
use Bookly\Lib\Slots\Booking;

/**
 * Class Calendar
 * @package BooklyOutlookCalendar\Lib\Microsoft
 */
class Calendar
{
    const EVENTS_PER_REQUEST = 250;
    const GUID = '{9dcd4815-2fe6-48f0-bbf0-e8e5518f0caf}';

    /** @var Client */
    protected $client;

    /**
     * Constructor.
     *
     * @param Client $client
     */
    public function __construct( Client $client )
    {
        $this->client = $client;
    }

    /**
     * Synchronize Outlook Calendar with given appointment.
     *
     * @param Appointment $appointment
     * @return bool
     */
    public function syncAppointment( Appointment $appointment )
    {
        if ( ! $this->_hasCalendar() ) {
            return true;
        }

        try {
            $event = $this->_populateEvent( new Vendor\Model\Event(), $appointment );
            if ( $appointment->hasOutlookCalendarEvent() ) {
                // Update event.
                $event = $this->client->graph()
                    ->createRequest( 'PATCH', sprintf(
                        '/me/calendars/%s/events/%s',
                        $this->_getCalendarId(),
                        $appointment->getOutlookEventId()
                    ) )
                    ->attachBody( $event )
                    ->setReturnType( get_class( $event ) )
                    ->execute()
                ;
            } else {
                // Create event.
                $event = $this->client->graph()
                    ->createRequest( 'POST', sprintf(
                        '/me/calendars/%s/events',
                        $this->_getCalendarId()
                    ) )
                    ->attachBody( $event )
                    ->setReturnType( get_class( $event ) )
                    ->execute()
                ;
            }
            $appointment
                ->setOutlookEventId( $event->getId() )
                ->setOutlookEventChangeKey( $event->getChangeKey() )
                ->save()
            ;

            return true;

        } catch ( \Exception $e ) {
            $this->client->addError( $e->getMessage() );
        }

        return false;
    }

    /**
     * Delete Outlook Calendar event by ID.
     *
     * @param string $event_id
     * @return bool
     */
    public function deleteEvent( $event_id )
    {
        if ( ! $this->_hasCalendar() ) {
            return true;
        }

        try {
            $this->client->graph()
                ->createRequest( 'DELETE', sprintf(
                    '/me/calendars/%s/events/%s',
                    $this->_getCalendarId(),
                    $event_id
                ) )
                ->execute()
            ;

            return true;

        } catch ( \Exception $e ) {
            $this->client->addError( $e->getMessage() );
        }

        return false;
    }

    /**
     * Get bookings created from Outlook Calendar events.
     *
     * @param DatePoint $start_date
     * @return Booking[]|false
     */
    public function getBookings( DatePoint $start_date )
    {
        if ( ! $this->_hasCalendar() ) {
            return array();
        }

        try {
            $result  = array();
            $request = $this->client->graph()
                ->createCollectionRequest( 'GET', sprintf(
                    '/me/calendars/%s/calendarView?startDateTime=%s&endDateTime=%s&$expand=%s&$orderby=%s',
                    $this->_getCalendarId(),
                    rawurlencode( $start_date->format( \DateTime::RFC3339 ) ),
                    rawurlencode( $start_date->modify( '+1 year' )->format( \DateTime::RFC3339 ) ),
                    rawurlencode( 'singleValueExtendedProperties($filter=id eq \'Integer ' . self::GUID . ' Name appointment_id\')' ),
                    rawurlencode( 'start/dateTime' )
                ) )
                ->setReturnType( 'BooklyOutlookCalendar\Lib\Microsoft\Vendor\Model\Event' )
                ->setPageSize( get_option( 'bookly_oc_limit_events' ) ?: self::EVENTS_PER_REQUEST )
            ;

            $series_events = array();

            do {
                // Fetch events.
                $events = $request->getPage();

                /** @var Vendor\Model\Event $event */
                foreach ( $events as $event ) {
                    if ( $event->isRemoved() ) {
                        continue;
                    }

                    if ( $event->getType()->is( Vendor\Model\EventType::SERIES_MASTER ) ) {
                        $series_events[ $event->getId() ] = $event;
                        continue;
                    } else {
                        if ( $event->getType()->is( Vendor\Model\EventType::OCCURRENCE ) ) {
                            if ( isset ( $series_events[ $event->getSeriesMasterId() ] ) ) {
                                /** @var Vendor\Model\Event $event2 */
                                $event2 = clone $series_events[ $event->getSeriesMasterId() ];
                                $event2->setId( $event->getId() );
                                $event2->setStart( $event->getStart() );
                                $event2->setEnd( $event->getEnd() );
                                $event2->setSeriesMasterId( $event->getSeriesMasterId() );
                                $event = $event2;
                            } else {
                                continue;
                            }
                        }
                    }

                    if ( ! $event->getShowAs()->is( Vendor\Model\FreeBusyStatus::FREE ) ) {
                        $ext_properties = $event->getSingleValueExtendedProperties();
                        if ( $ext_properties !== null ) {
                            foreach ( $ext_properties as $property ) {
                                if ( $property['id'] == 'Integer ' . self::GUID . ' Name appointment_id') {
                                    // Skip events created by Bookly.
                                    continue 2;
                                }
                            }
                        }

                        // Get start/end dates of event and transform them into WP time zone.
                        $event_start = $event->getStart();
                        $event_end   = $event->getEnd();

                        if ( $event->getIsAllDay() ) {
                            // All day event (consider it as in WP time zone).
                            $event_start_date = date_create( $event_start->getDateTime() );
                            $event_end_date   = date_create( $event_end->getDateTime() );
                        } else {
                            // Regular event.
                            $event_start_date = date_timestamp_set( date_create( Config::getWPTimeZone() ), strtotime( $event_start->getDateTime() ) );
                            $event_end_date   = date_timestamp_set( date_create( Config::getWPTimeZone() ), strtotime( $event_end->getDateTime()  ) );
                        }

                        // Populate result.
                        $result[] = new Booking(
                            0,
                            0,
                            1,
                            0,
                            $event_start_date->format( 'Y-m-d H:i:s' ),
                            $event_end_date->format( 'Y-m-d H:i:s' ),
                            0,
                            0,
                            0,
                            0,
                            true
                        );
                    }
                }

            } while ( ! $request->isEnd() );

            return $result;

        } catch ( \Exception $e ) {
            $this->client->addError( $e->getMessage() );
        }

        return false;
    }

    /**
     * Perform incremental synchronization of appointments with Outlook Calendar.
     *
     * @return bool
     */
    public function sync()
    {
        if ( ! $this->_hasCalendar() ) {
            return true;
        }

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

        try {
            $request = $this->client->graph()
                ->createCollectionRequest( 'GET', $this->_getDeltaLink() )
                ->setReturnType( 'BooklyOutlookCalendar\Lib\Microsoft\Vendor\Model\Event' )
                ->addHeaders( array( 'Prefer' => 'odata.maxpagesize=' . self::EVENTS_PER_REQUEST ) )
            ;

            $series_events = array();

            do {
                // Fetch events.
                $events = $request->getPage();

                /** @var Vendor\Model\Event $event */
                foreach ( $events as $event ) {
                    $action = null;

                    if ( $event->isRemoved() ) {
                        $action = 'delete';
                    } else {
                        if ( $event->getType()->is( Vendor\Model\EventType::SERIES_MASTER ) ) {
                            $series_events[ $event->getId() ] = $event;
                            continue;
                        } else {
                            if ( $event->getType()->is( Vendor\Model\EventType::OCCURRENCE ) ) {
                                if ( isset ( $series_events[ $event->getSeriesMasterId() ] ) ) {
                                    /** @var Vendor\Model\Event $event2 */
                                    $event2 = clone $series_events[ $event->getSeriesMasterId() ];
                                    $event2->setId( $event->getId() );
                                    $event2->setStart( $event->getStart() );
                                    $event2->setEnd( $event->getEnd() );
                                    $event2->setSeriesMasterId( $event->getSeriesMasterId() );
                                    $event = $event2;
                                } else {
                                    continue;
                                }
                            }
                            if ( $event->getShowAs()->is( Vendor\Model\FreeBusyStatus::FREE ) ) {
                                $action = 'delete';
                            } else {
                                $action = 'create/update';
                            }
                        }
                    }

                    switch ( $action ) {
                        case 'create/update':
                            // Create/update appointment.
                            $appointment = $this->_findAppointmentForEvent( $event );
                            if ( $appointment && $appointment->getOutlookEventChangeKey() == $event->getChangeKey() ) {
                                // Skip event with existing change key.
                                continue;
                            }
                            $this->_populateAppointment( $appointment ?: new Appointment(), $event )->save();
                            break;
                        case 'delete':
                            // Delete appointment if event does not block time on Outlook Calendar anymore.
                            $appointment = $this->_findAppointmentForEvent( $event );
                            if ( $appointment ) {
                                $appointment->setOutlookEventId( null )->delete();
                            } else {
                                // If no appointment was found try to find series occurrences.
                                $appointments = $this->_findAppointmentsForSeriesEvent( $event );
                                foreach ( $appointments as $appointment ) {
                                    $appointment->setOutlookEventId( null )->delete();
                                }
                            }
                            break;
                    }
                }

            } while ( ! $request->isEnd() );

            // Save next sync token.
            $this->_setDeltaLink( $request->getDeltaLink() )->_saveData();

            // Delete orphan appointments for each series event.
            foreach ( $series_events as $event ) {
                $appointments = $this->_findAppointmentsForSeriesEvent( $event, true );
                foreach ( $appointments as $appointment ) {
                    $appointment->setOutlookEventId( null )->delete();
                }
            }

            return true;

        } catch ( \Exception $e ) {
            $this->client->addError( $e->getMessage() );
        }

        return false;
    }

    /**
     * Clear delta link.
     *
     * @return $this
     */
    public function clearDeltaLink()
    {
        return $this->_setDeltaLink( null );
    }

    /**
     * Create notification subscription.
     *
     * @return bool
     */
    public function createSubscription()
    {
        if ( ! $this->_hasCalendar() ) {
            return true;
        }

        try {
            if ( $this->_hasSubscription() ) {
                // Stop existing subscription.
                $this->deleteSubscription( false );
            }
            // Create new subscription.
            $client_state = uniqid( 'bookly-' . $this->client->staff()->getId() . '-', true );
            $subscription = new Vendor\Model\Subscription();
            $subscription
                ->setChangeType( 'created,updated,deleted' )
                ->setNotificationUrl( admin_url( 'admin-ajax.php?action=bookly_outlook_calendar_push_notifications', 'https' ) )
                ->setResource( sprintf( 'me/calendars/%s/events', $this->_getCalendarId() ) )
                ->setExpirationDateTime( date_create( '+4000 minutes' ) )
                ->setClientState( $client_state )
            ;

            /** @var Vendor\Model\Subscription $subscription */
            $subscription = $this->client->graph()
                ->createRequest( 'POST', '/subscriptions' )
                ->attachBody( $subscription )
                ->setReturnType( get_class( $subscription ) )
                ->execute()
            ;

            // Save subscription data.
            $this
                ->_setSubscriptionId( $subscription->getId() )
                ->_setSubscriptionExpirationDateTime( $subscription->getExpirationDateTime()->format( \DateTime::RFC3339 ) )
                ->_setSubscriptionClientState( $client_state )
                ->_saveData()
            ;

            return true;

        } catch ( \Exception $e ) {
            $this->client->addError( $e->getMessage() );
        }

        return false;
    }

    /**
     * Renew subscription.
     *
     * @return bool
     */
    public function renewSubscription()
    {
        if ( ! $this->_hasCalendar() ) {
            return true;
        }

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

        try {
            // Renew subscription.
            $subscription = new Vendor\Model\Subscription();
            $subscription
                ->setExpirationDateTime( date_create( '+4000 minutes' ) )
            ;
            /** @var Vendor\Model\Subscription $subscription */
            $subscription = $this->client->graph()
                ->createRequest( 'PATCH', sprintf( '/subscriptions/%s', $this->_getSubscriptionId() ) )
                ->attachBody( $subscription )
                ->setReturnType( get_class( $subscription ) )
                ->execute()
            ;

            // Update subscription data.
            $this
                ->_setSubscriptionExpirationDateTime( $subscription->getExpirationDateTime()->format( \DateTime::RFC3339 ) )
                ->_saveData()
            ;

            return true;

        } catch ( \Exception $e ) {
            $this->client->addError( $e->getMessage() );
        }

        return false;
    }

    /**
     * Delete notification subscription.
     *
     * @param bool $update_staff
     * @return bool
     */
    public function deleteSubscription( $update_staff = true )
    {
        if ( ! $this->_hasSubscription() ) {
            return true;
        }

        try {
            $this->client->graph()
                ->createRequest( 'DELETE', sprintf( '/subscriptions/%s', $this->_getSubscriptionId() ) )
                ->execute()
            ;

            // Delete subscription data.
            $this
                ->_setSubscriptionId( null )
                ->_setSubscriptionExpirationDateTime( null )
            ;

            if ( $update_staff ) {
                $this->_saveData();
            }

            return true;

        } catch ( \Exception $e ) {
            $this->client->addError( $e->getMessage() );
        }

        return false;
    }


    /**
     * Populate Outlook Calendar event with data from given appointment.
     *
     * @param Vendor\Model\Event $event
     * @param Appointment $appointment
     * @return Vendor\Model\Event
     * @throws
     */
    protected function _populateEvent( Vendor\Model\Event $event, Appointment $appointment )
    {
        // Set start and end dates.
        $start_datetime = new Vendor\Model\DateTimeTimeZone();
        $start_datetime
            ->setDateTime( DatePoint::fromStr( $appointment->getStartDate() )->format( \DateTime::RFC3339 ) )
            ->setTimeZone( 'UTC' )
        ;
        $end_datetime = new Vendor\Model\DateTimeTimeZone();
        $end_datetime
            ->setDateTime( DatePoint::fromStr( $appointment->getEndDate() )->modify( (int) $appointment->getExtrasDuration() )->format( \DateTime::RFC3339 ) )
            ->setTimeZone( 'UTC' )
        ;
        $event->setStart( $start_datetime );
        $event->setEnd( $end_datetime );

        // Set other fields.
        if ( $appointment->getCreatedFrom() == 'bookly' ) {
            // Populate event created from Bookly.
            if ( $appointment->getServiceId() ) {
                $service = Service::find( $appointment->getServiceId() );
                $service_capacity = 0;
            } else {
                // Custom service.
                $service = new Service();
                $service
                    ->setTitle( $appointment->getCustomServiceName() )
                    ->setPrice( $appointment->getCustomServicePrice() )
                ;
                $service_capacity = 9999;
            }
            $content      = '';
            $client_names = array();
            $client_tpl   = get_option( 'bookly_oc_event_client_info' );
            $signed_up    = 0;
            $codes        = null;
            foreach ( $appointment->getCustomerAppointments() as $ca ) {
                $order = Order::create( Customer::find( $ca->getCustomerId() ) );
                if ( $ca->getPaymentId() ) {
                    $order->setPayment( Payment::find( $ca->getPaymentId() ) );
                }
                $item  = Simple::create( $ca )
                    ->setAppointment( $appointment )
                    ->setService( $service )
                ;
                $codes = new Codes( $order );
                $codes->prepareForItem( $item, 'staff' );
                $content .= $codes->replace( $client_tpl ) . PHP_EOL;
                $signed_up += $ca->getNumberOfPersons();
                $client_names[] = $ca->customer->getFullName();
                if ( $service_capacity == 0 ) {
                    $result = StaffService::query()
                        ->select( 'capacity_max' )
                        ->where( 'staff_id',   $item->getStaff()->getId() )
                        ->where( 'service_id', $service->getId() )
                        ->fetchRow()
                    ;
                    if ( $result ) {
                        $service_capacity = $result['capacity_max'];
                    }
                }
            }
            $appointment_info = strtr( get_option( 'bookly_oc_event_appointment_info' ), array(
                '{signed_up}'         => $signed_up,
                '{number_of_persons}' => $signed_up,
                '{service_capacity}'  => $service_capacity,
                '{client_names}'      => implode( ', ', $client_names ),
                '{on_waiting_list}'   => Config::waitingListActive()
                    ? CustomerAppointment::query( 'ca' )
                        ->where( 'ca.appointment_id', $appointment->getId() )
                        ->where( 'status', CustomerAppointment::STATUS_WAITLISTED )
                        ->count()
                    : 0,
            ) );
            if ( $codes == null ) {
                $order = Order::create( new Customer() );
                $item  = Simple::create( new CustomerAppointment() )
                    ->setAppointment( $appointment )
                    ->setService( $service )
                ;
                $codes = new Codes( $order );
                $codes->prepareForItem( $item, 'staff' );
            }
            $content = trim( $codes->replace( $appointment_info ) . PHP_EOL . $content );

            $staff = Staff::find( $appointment->getStaffId() );
            $subject = strtr( get_option( 'bookly_oc_event_title', '{service_name}' ), array(
                '{service_name}' => $service->getTitle(),
                '{client_names}' => implode( ', ', $client_names ),
                '{staff_name}'   => $staff->getFullName(),
            ) );

            $event->setSubject( $subject );
            $body = new Vendor\Model\ItemBody();
            $body
                ->setContentType( new Vendor\Model\BodyType( Vendor\Model\BodyType::TEXT ) )
                ->setContent( $content )
            ;
            $event->setBody( $body );
            $property = new Vendor\Model\SingleValueLegacyExtendedProperty();
            $property
                ->setId( 'Integer ' . self::GUID . ' Name appointment_id' )
                ->setValue( (string) $appointment->getId() )
            ;
            $event->setSingleValueExtendedProperties( array( $property ) );
        } else if ( get_option( 'bookly_oc_full_sync_titles', 1 ) ) {
            // Populate event created from Outlook Calendar.
            if ( $appointment->getServiceId() ) {
                $service = Service::find( $appointment->getServiceId() );
                $staff = Staff::find( $appointment->getStaffId() );
                $client_names = array();
                foreach ( $appointment->getCustomerAppointments() as $ca ) {
                    $client_names[] = $ca->customer->getFullName();
                }
                $subject = strtr( get_option( 'bookly_oc_event_title', '{service_name}' ), array(
                    '{service_name}' => $service->getTitle(),
                    '{client_names}' => implode( ', ', $client_names ),
                    '{staff_name}'   => $staff->getFullName(),
                ) );
                $event->setSubject( $subject );
            } else {
                // Custom service.
                $event->setSubject( $appointment->getCustomServiceName() );
            }
        }

        return $event;
    }

    /**
     * Perform full 2-way synchronization between appointments and Outlook Calendar.
     *
     * @return bool
     */
    protected function _fullSync()
    {
        $start_date = DatePoint::fromStr( sprintf( '-%d days midnight', get_option( 'bookly_oc_full_sync_offset_days', 0 ) ) );

        // 1. Delete appointments created from Outlook Calendar.
        Appointment::query()
            ->delete()
            ->where( 'staff_id', $this->client->staff()->getId() )
            ->where( 'created_from', 'outlook' )
            ->whereGt( 'end_date', $start_date->format( 'Y-m-d H:i:s' ) )
            ->execute()
        ;

        // 2. Delete Outlook Calendar events created from Bookly.
        if ( $this->_deleteEventsCreatedFromBookly( $start_date ) ) {
            // 3. Copy Outlook Calendar events to appointments.
            if ( $this->_copyEventsToAppointments( $start_date ) ) {
                // 4. Copy appointments to Outlook Calendar events.
                $appointments = Appointment::query()
                    ->where( 'staff_id', $this->client->staff()->getId() )
                    ->where( 'created_from', 'bookly' )
                    ->whereGt( 'end_date', $start_date->format( 'Y-m-d H:i:s' ) )
                    ->find()
                ;
                /** @var Appointment $appointment */
                foreach ( $appointments as $appointment ) {
                    if ( ! $this->syncAppointment( $appointment->setOutlookEventId( null ) ) ) {
                        return false;
                    }
                }

                return true;
            }

        }

        return false;
    }

    /**
     * Find associated appointment for given Outlook Calendar event.
     *
     * @param Vendor\Model\Event $event
     * @return Appointment|false
     */
    protected function _findAppointmentForEvent( Vendor\Model\Event $event )
    {
        $appointment = new Appointment();
        $appointment->loadBy( array(
            'staff_id'         => $this->client->staff()->getId(),
            'outlook_event_id' => $event->getId(),
        ) );

        return $appointment->isLoaded() ? $appointment : false;
    }

    /**
     * Find associated appointments for given Outlook Calendar series master event.
     *
     * @param Vendor\Model\Event $event
     * @param bool $orphan  Whether to search for orphan/outdated appointments only
     * @return Appointment[]
     */
    protected function _findAppointmentsForSeriesEvent( Vendor\Model\Event $event, $orphan = false )
    {
        $query = Appointment::query()
            ->where( 'staff_id', $this->client->staff()->getId() )
            ->where( 'outlook_event_series_id', $event->getId() )
        ;

        if ( $orphan ) {
            $query->whereNot( 'outlook_event_change_key', $event->getChangeKey() );
        }

        return $query->find();
    }

    /**
     * Copy Outlook Calendar events to appointments starting from given date.
     *
     * @param DatePoint $start_date
     * @return bool
     */
    protected function _copyEventsToAppointments( DatePoint $start_date )
    {
        try {
            $request = $this->client->graph()
                ->createCollectionRequest( 'GET', sprintf(
                    '/me/calendars/%s/calendarView/delta?startDateTime=%s&endDateTime=%s',
                    $this->_getCalendarId(),
                    rawurlencode( $start_date->format( \DateTime::RFC3339 ) ),
                    rawurlencode( $start_date->modify( '+100 years' )->format( \DateTime::RFC3339 ) )
                ) )
                ->setReturnType( 'BooklyOutlookCalendar\Lib\Microsoft\Vendor\Model\Event' )
                ->addHeaders( array( 'Prefer' => 'odata.maxpagesize=' . self::EVENTS_PER_REQUEST ) )
            ;

            $series_events = array();

            do {
                // Fetch events.
                $events = $request->getPage();

                /** @var Vendor\Model\Event $event */
                foreach ( $events as $event ) {
                    if ( $event->isRemoved() ) {
                        continue;
                    }

                    if ( $event->getType()->is( Vendor\Model\EventType::SERIES_MASTER ) ) {
                        $series_events[ $event->getId() ] = $event;
                        continue;
                    } else {
                        if ( $event->getType()->is( Vendor\Model\EventType::OCCURRENCE ) ) {
                            if ( isset ( $series_events[ $event->getSeriesMasterId() ] ) ) {
                                /** @var Vendor\Model\Event $event2 */
                                $event2 = clone $series_events[ $event->getSeriesMasterId() ];
                                $event2->setId( $event->getId() );
                                $event2->setStart( $event->getStart() );
                                $event2->setEnd( $event->getEnd() );
                                $event2->setSeriesMasterId( $event->getSeriesMasterId() );
                                $event = $event2;
                            } else {
                                continue;
                            }
                        }
                    }

                    if ( ! $event->getShowAs()->is( Vendor\Model\FreeBusyStatus::FREE ) ) {
                        $this->_populateAppointment( new Appointment(), $event )->save();
                    }
                }

            } while ( ! $request->isEnd() );

            // Save delta link.
            $this->_setDeltaLink( $request->getDeltaLink() )->_saveData();

            return true;

        } catch ( \Exception $e ) {
            $this->client->addError( $e->getMessage() );
        }

        return false;
    }

    /**
     * Delete Outlook Calendar events created from Bookly starting from given date.
     *
     * @param DatePoint $start_date
     * @return bool
     */
    protected function _deleteEventsCreatedFromBookly( DatePoint $start_date )
    {
        try {
            $request = $this->client->graph()
                ->createCollectionRequest( 'GET', sprintf(
                    '/me/calendars/%s/events?$filter=%s',
                    $this->_getCalendarId(),
                    rawurlencode( sprintf(
                        "end/dateTime ge '%s' and singleValueExtendedProperties/Any(ep: ep/id eq 'Integer %s Name appointment_id' and cast(ep/value, Edm.Int32) gt 0)",
                        $start_date->format( \DateTime::RFC3339 ),
                        self::GUID
                    ) )
                ) )
                ->setReturnType( 'BooklyOutlookCalendar\Lib\Microsoft\Vendor\Model\Event' )
                ->setPageSize( self::EVENTS_PER_REQUEST )
            ;

            do {
                // Find events.
                $events = $request->getPage();

                /** @var Vendor\Model\Event $event */
                foreach ( $events as $event ) {
                    $this->client->graph()
                        ->createRequest( 'DELETE', sprintf(
                            '/me/calendars/%s/events/%s',
                            $this->_getCalendarId(),
                            $event->getId()
                        ) )
                        ->execute()
                    ;
                }

            } while ( ! $request->isEnd() );

            return true;

        } catch ( \Exception $e ) {
            $this->client->addError( $e->getMessage() );
        }

        return false;
    }

    /**
     * Populate appointment with data from given Outlook Calendar event.
     *
     * @param Appointment $appointment
     * @param Vendor\Model\Event $event
     * @return Appointment
     * @throws
     */
    protected function _populateAppointment( Appointment $appointment, Vendor\Model\Event $event )
    {
        // Get start/end dates of event and transform them into WP time zone.
        $event_start = $event->getStart();
        $event_end   = $event->getEnd();

        if ( $event->getIsAllDay() ) {
            // All day event (consider it as in WP time zone).
            $event_start_date = date_create( $event_start->getDateTime() );
            $event_end_date   = date_create( $event_end->getDateTime() );
        } else {
            // Regular event.
            $event_start_date = date_timestamp_set( date_create( Config::getWPTimeZone() ), strtotime( $event_start->getDateTime() ) );
            $event_end_date   = date_timestamp_set( date_create( Config::getWPTimeZone() ), strtotime( $event_end->getDateTime()  ) );
        }

        // Populate appointment.
        if ( ! $appointment->isLoaded() ) {
            $appointment->setCreatedFrom( 'outlook' );
        }
        if ( $appointment->getCreatedFrom() == 'outlook' ) {
            if ( get_option( 'bookly_oc_full_sync_titles', 1 ) ) {
                $appointment->setCustomServiceName( $event->getSubject() );
            } else if ( $appointment->getCustomServiceName() === null ) {
                $appointment->setCustomServiceName( esc_html__( 'Outlook Calendar event', 'bookly' ) );
            }
        }
        $appointment
            ->setStaff( $this->client->staff() )
            ->setStartDate( $event_start_date->format( 'Y-m-d H:i:s' ) )
            ->setEndDate( $event_end_date->format( 'Y-m-d H:i:s' ) )
            ->setOutlookEventId( $event->getId() )
            ->setOutlookEventChangeKey( $event->getChangeKey() )
            ->setOutlookEventSeriesId( $event->getSeriesMasterId() )
        ;

        return $appointment;
    }

    /**
     * Get Outlook Calendar ID.
     *
     * @return string
     */
    public function _getCalendarId()
    {
        return $this->client->data()->calendar->id;
    }

    /**
     * Get delta link for Outlook Calendar.
     *
     * @return string|null
     */
    protected function _getDeltaLink()
    {
        return $this->client->data()->calendar->delta_link;
    }

    /**
     * Set delta link for Outlook Calendar.
     *
     * @param string $delta_link
     * @return $this
     */
    protected function _setDeltaLink( $delta_link )
    {
        $this->client->data()->calendar->delta_link = $delta_link;

        return $this;
    }

    /**
     * Get subscription ID for watched resource.
     *
     * @return string|null
     */
    public function _getSubscriptionId()
    {
        return $this->client->data()->subscription->id;
    }

    /**
     * Set subscription ID for watched resource.
     *
     * @param string $id
     * @return $this
     */
    protected function _setSubscriptionId( $id )
    {
        $this->client->data()->subscription->id = $id;

        return $this;
    }

    /**
     * Get subscription expiration date and time.
     *
     * @return int
     */
    protected function _getSubscriptionExpirationDateTime()
    {
        return $this->client->data()->subscription->expiration_date_time;
    }

    /**
     * Set subscription expiration date and time.
     *
     * @param string $expiration_date_time
     * @return $this
     */
    protected function _setSubscriptionExpirationDateTime( $expiration_date_time )
    {
        $this->client->data()->subscription->expiration_date_time = $expiration_date_time;

        return $this;
    }

    /**
     * Get subscription client state.
     *
     * @return string|null
     */
    public function _getSubscriptionClientState()
    {
        return $this->client->data()->subscription->client_state;
    }

    /**
     * Set subscription client state.
     *
     * @param string $client_state
     * @return $this
     */
    protected function _setSubscriptionClientState( $client_state )
    {
        $this->client->data()->subscription->client_state = $client_state;

        return $this;
    }

    /**
     * Tells whether there is a selected calendar.
     *
     * @return bool
     */
    protected function _hasCalendar()
    {
        return $this->_getCalendarId() != '';
    }

    /**
     * Tells whether there is a delta link.
     *
     * @return bool
     */
    protected function _hasDeltaLink()
    {
        return $this->_getDeltaLink() != '';
    }

    /**
     * Tells whether there is a configured notification subscription.
     *
     * @return bool
     */
    protected function _hasSubscription()
    {
        return $this->_getSubscriptionId() != '';
    }

    /**
     * Save current Microsoft data to associated staff.
     *
     * @return $this
     */
    protected function _saveData()
    {
        $this->client->staff()->setOutlookData( $this->client->data()->toJson() )->save();

        return $this;
    }
}