<?php
/**
 * Simply Schedule Appointments Appointment Object.
 *
 * @since   0.0.3
 * @package Simply_Schedule_Appointments
 */

use League\Period\Period;

/**
 * Simply Schedule Appointments Appointment Object.
 *
 * @since 0.0.3
 */
class SSA_Appointment_Object {
	protected $id = null;
	protected $model = null;
	protected $data = null;
	protected $appointment_type;
	protected $staff_members;
	protected $resources;
	protected $recursive_fetched = -2;

	/**
	 * Per-request cache: raw data arrays keyed by appointment ID.
	 * Populated by from_data() so any subsequent new SSA_Appointment_Object( $id )
	 * skip DB queries for data already fetched in bulk.
	 *
	 * @var array
	 */
	protected static $data_cache = array();

	protected $status;

	/**
	 * Parent plugin class.
	 *
	 * @since 0.0.3
	 *
	 * @var   Simply_Schedule_Appointments
	 */
	protected $plugin = null;

	/**
	 * Constructor.
	 *
	 * @since  0.0.3
	 *
	 * @param  Simply_Schedule_Appointments $plugin Main plugin object.
	 */
	public function __construct( $id ) {
		if ( $id === 'transient' ) {		
			return;
		}

		$this->id = $id;
	}

	public static function instance( $appointment ) {
		if ( $appointment instanceof SSA_Appointment_Object ) {
			return $appointment;
		}

		if ( is_array( $appointment ) ) {
			$appointment = new SSA_Appointment_Object( $appointment['id'] );
			return $appointment;
		}

		$appointment = new SSA_Appointment_Object( $appointment );
		return $appointment;
	}

	/**
	 * Factory function to create from a pre-loaded data array.
	 * Bypasses lazy loading — avoids N+1 queries when building collections.
	 *
	 * @param array $data Full appointment data array (must include 'id').
	 * @param int   $recursive_level The recursive level of the pre-loaded data (default 0 for flat query results).
	 * @return SSA_Appointment_Object
	 */
	public static function from_data( array $data, int $recursive_level = 0 ): self {
		$obj                    = new self( $data['id'] );
		$obj->data              = $data;
		$obj->recursive_fetched = $recursive_level;
		// Pre-populate the request cache so any new SSA_Appointment_Object( $id )
		// created later (e.g. inside template-var filters) skips the DB entirely.
		self::$data_cache[ $data['id'] ] = array(
			'data'  => $data,
			'level' => $recursive_level,
		);
		return $obj;
	}

	/**
	 * Factory function to create with explicit data
	 *
	 * @param array $data
	 * @return SSA_Appointment_Object
	 * @author 
	 **/
	public static function create( SSA_Appointment_Type_Object $appointment_type, array $data ) {
		$appointment_object = new SSA_Appointment_Object( 'transient' );
		$appointment_object->appointment_type = $appointment_type;
		$data['appointment_type_id'] = $appointment_type->id;
		$appointment_object->data = $data;
		return $appointment_object;
	}

	/**
	 * Magic getter for our object.
	 *
	 * @since  0.0.0
	 *
	 * @param  string $field Field to get.
	 * @throws Exception     Throws an exception if the field is invalid.
	 * @return mixed         Value of the field.
	 */
	public function __get( $field ) {
		if ( empty( $this->data ) && $field !== 'id' ) {
			$this->get();
		}

		switch ( $field ) {
			case 'id':
			case 'data':
				return $this->$field;
			case 'start_date_datetime':
			case 'end_date_datetime':
				$date_time = ssa_datetime( $this->data[str_replace('_datetime', '', $field)] );
				return $date_time;
			case 'start_date_timestamp':
			case 'end_date_timestamp':
				return ssa_gmtstrtotime( $this->data[str_replace('_timestamp', '', $field)] );
			case 'web_meeting_url':
				if ( isset( $this->data[$field] ) ) {
					return trim( $this->data[$field] );
				}
			case 'web_meeting_id':
				if ( isset( $this->data[$field] ) ) {
					return trim( $this->data[$field] );
				}
			case 'web_meeting_password':
				if ( isset( $this->data[$field] ) ) {
					return trim( $this->data[$field] );
				}
			default:
				if ( isset( $this->data[$field] ) ) {
					return $this->data[$field];
				}
				
				throw new Exception( 'Invalid ' . __CLASS__ . ' property: ' . $field );
		}
	}

	public function refresh( $recursive = -1 ) {
		$this->recursive_fetched = -2;
		$this->get( $recursive );
	}

	public function get( $recursive = -1 ) {
		if ( $recursive > $this->recursive_fetched ) {
			// Read-only check of the pre-load cache (written exclusively by from_data()).
			// from_data() is only called during bulk ICS fetches, so for every other code
			// path in the app this cache is empty and this check is a no-op.
			if ( isset( self::$data_cache[ $this->id ] ) && $recursive <= self::$data_cache[ $this->id ]['level'] ) {
				$this->data             = array_merge( $this->data ?? array(), self::$data_cache[ $this->id ]['data'] );
				$this->recursive_fetched = self::$data_cache[ $this->id ]['level'];
				return;
			}

			if ( null === $this->data ) {
				$this->data = array();
			}

			$model_data = ssa()->appointment_model->get( $this->id, $recursive );
			if ( empty( $model_data['id'] ) ) {
				if ( ! isset( $model_data['id'] ) || ! in_array( $model_data['id'], array( '0', 0 ), true ) ) {
					throw new Exception( 'Appointment ID ' . $this->id . ' not found' );
				}
			}
			$this->data = array_merge( $this->data, $model_data );
			$this->recursive_fetched = $recursive;
			// Intentionally NOT writing back to $data_cache here.
			// The cache is exclusively managed by from_data() for bulk ICS pre-loading.
		}
	}

	public function get_appointment_period() {
		$start_date = $this->__get( 'start_date' );
		$end_date = $this->__get( 'end_date' );

		if ( $end_date < $start_date ) {
			ssa_debug_log( __CLASS__ . ' ' . __FUNCTION__ . '():' . __LINE__ );
			ssa_debug_log( 'The ending datepoint must be greater or equal to the starting datepoint in appointment ID ' . $this->id, 10 );

			return new Period(
				$start_date,
				$start_date
			);
		}

		return new Period(
			$start_date,
			$end_date
		);
	}

	public function get_buffer_before_period() {
		$buffer_before = $this->get_appointment_type()->buffer_before;
		if ( empty( $buffer_before ) ) {
			return false;
		}

		$start_date = ssa_datetime( $this->__get( 'start_date' ) );
		$calculated_period = new Period( $start_date->sub( new DateInterval( 'PT'.absint( $buffer_before ).'M' ) ), $start_date );
		
		return $calculated_period;
	}

	public function get_buffer_after_period() {
		$buffer_after = $this->get_appointment_type()->buffer_after;
		if ( empty( $buffer_after ) ) {
			return false;
		}

		$end_date = ssa_datetime( $this->__get( 'end_date' ) );
		$calculated_period = new Period( $end_date, $end_date->add( new DateInterval( 'PT'.absint( $buffer_after ).'M' ) ) );
		
		return $calculated_period;
	}

	public function get_buffered_period() {
		$period = $this->get_appointment_period();

		$buffer_before_period = $this->get_buffer_before_period();
		$buffer_after_period = $this->get_buffer_after_period();

		if ( false === $buffer_before_period && false === $buffer_after_period ) {
			return $period;
		}

		if ( false !== $buffer_before_period ) {
			if ( $buffer_before_period->getStartDate() < $period->getStartDate() ) {
				$period = new Period( $buffer_before_period->getStartDate(), $period->getEndDate() );
			}
			// TODO: else { Log error (we should never end up here), but at least we have prevented a fatal error }
		}
		if ( false !== $buffer_after_period ) {
			if ( $period->getEndDate() < $buffer_after_period->getEndDate() ) {
				$period = new Period( $period->getStartDate(), $buffer_after_period->getEndDate() );
			}
			// TODO: else { Log error (we should never end up here), but at least we have prevented a fatal error }
		}

		return $period;
	}

	public function get_buffered_query_period() {
		$period = $this->get_appointment_period();

		$buffer_before = $this->get_appointment_type()->buffer_before;
		$buffer_after = $this->get_appointment_type()->buffer_after;
		$buffer_max = max( $buffer_before, $buffer_after );

		if ( empty( $buffer_max ) ) {
			return $period;
		}
		
		$start_date = ssa_datetime($this->__get('start_date'));
		$end_date = ssa_datetime($this->__get('end_date'));
		$period = new Period(
			$start_date->sub(new DateInterval('PT' . absint($buffer_max) . 'M')),
			$end_date->add(new DateInterval('PT' . absint($buffer_max) . 'M'))
		);

		return $period;
	}


	public function get_data( $recursive = -1, $fetch_fields = array() ) {
		$this->get( $recursive );

		if ( $recursive >= 0 ) {
			if ( !isset( $fetch_fields['public_edit_url'] ) ) {
				$fetch_fields['public_edit_url'] = true;
			}
			if ( !isset( $fetch_fields['date_timezone'] ) ) {
				$fetch_fields['date_timezone'] = true;
			}
		}

		if ( !empty( $fetch_fields ) ) {
			$this->fetch_fields( $fetch_fields );
		}

		return $this->data;
	}

	public function fetch_fields( $fetch_fields = array() ) {
		if ( !is_array( $fetch_fields ) ) {
			throw new SSA_Exception("$fetch_fields must be an array", 1);
		}

		foreach ( $fetch_fields as $fetch_field => $fetch_options ) {
			if ( is_int( $fetch_field ) ) {
				$fetch_field = $fetch_options;
				$fetch_options = array();
			}

			$method_name = 'fetch_'.$fetch_field;
			if ( !method_exists( $this, $method_name ) ) {
				throw new SSA_Exception(__CLASS__ . "->" . $method_name . "() not implemented", 1);
			}

			$this->$method_name( $fetch_options );
		}
	}

	public function fetch_add_to_calendar_links( $atts = array() ) {
		if ( !is_array( $atts ) ) {
			$atts = array();
		}

		$atts = shortcode_atts( array(
			'customer' => true,
		), $atts );

		if ( !empty( $atts['customer'] ) ) {
			$this->data['ics']['customer']  = $this->get_ics_download_url( 'customer' );
			$this->data['gcal']['customer'] = $this->get_gcal_add_link( 'customer' );
		}
	}

	public function fetch_date_timezone( $atts = array() ) {
		$this->data['date_timezone'] = $this->get_date_timezone();
	}

	public function fetch_public_edit_url( $atts = array() ) {
		$this->data['public_edit_url'] = $this->get_public_edit_url();
	}

	public function get_appointment_type() {
		if ( ! empty( $this->appointment_type ) ) {
			return $this->appointment_type;
		}

		$this->appointment_type = new SSA_Appointment_Type_Object( $this->appointment_type_id );
		return $this->appointment_type;
	}


	public function get_label_id() {
		return ssa()->appointment_type_model->get_label_id_for_appointment_type_id( $this->appointment_type_id );
	}

	public function get_staff_members() {
		if ( ! empty( $this->staff_members ) ) {
			return $this->staff_members;
		}

		$is_enabled = ssa()->settings_installed->is_enabled( 'staff' );
		if ( ! $is_enabled ) {
			return;
		}

		$staff_ids = ssa()->staff_appointment_model->get_staff_ids( $this->id );
		if ( empty( $staff_ids ) ) {
			$this->staff_members = null;
			return;
		}

		$staff_members = array();
		foreach ($staff_ids as $staff_id) {
			$staff_members[] = new SSA_Staff_Object( $staff_id );
		}
		$this->staff_members = $staff_members;

		return $this->staff_members;
	}

	public function get_date_timezone( $for_type = '', $for_id = '' ) {
		if ( $for_type === 'staff' ) {
			// TODO: Customize for staff ID
			// TODO: Customize for location ID
			// TODO: Customize for customer
			// TODO: Customize for admin
		} else {
			$settings = ssa()->settings->get();
			$date_timezone = SSA_Utils::safe_timezone( $settings['global']['timezone_string'] );
		}

		return $date_timezone;
	}

	public function get_customer_name() {
		$customer_information = $this->__get( 'customer_information' );
		$customer_name = '';
		if ( ! empty( $customer_information['name'] ) ) {
			$customer_name = $customer_information['name'];
		} elseif ( ! empty( $customer_information['Name'] ) ) {
			$customer_name = $customer_information['Name'];
		}

		return $customer_name;
	}

	public function get_customer_email() {
		$customer_information = $this->__get( 'customer_information' );
		$customer_email = '';
		if ( ! empty( $customer_information['email'] ) ) {
			$customer_email = $customer_information['email'];
		} elseif ( ! empty( $customer_information['Email'] ) ) {
			$customer_email = $customer_information['Email'];
		}

		return $customer_email;
	}

	/**
	 * Given an appointment, get the customer timezone string.
	 *
	 * @since 4.9.1
	 *
	 * @return string
	 */
	public function get_customer_timezone_string() {
		$customer_timezone = $this->__get( 'customer_timezone' );
		return $customer_timezone;
	}

	/**
	 * Given an appointment, get the customer timezone object.
	 *
	 * @since 4.9.1
	 *
	 * @return DateTimeZone
	 */
	public function get_customer_timezone() {
		$customer_timezone = $this->get_customer_timezone_string();

		if ( empty( $customer_timezone ) ) {
			return null;
		}
		return SSA_Utils::safe_timezone( $customer_timezone );
	}

	/**
	 * Given a specific recipient type, get's the right twig template to
	 * populate the event title, and parses the content.
	 *
	 * @since 5.4.0
	 *
	 * @param SSA_Recipient $recipient The recipient object.
	 * @return string the event title.
	 */
	public function get_calendar_event_title( SSA_Recipient $recipient ) {
		$event_type     = $this->get_calendar_event_type( $recipient );
		$calendar_event = new SSA_Calendar_Events_Object( $event_type );

		$title = $calendar_event->get_calendar_event_content( 'title', $this->id, true );

		// if appointment is canceled, prefix with "Canceled: ".
		if ( $this->is_individual_appointment() && $this->is_canceled() ) {
			$title = __('Canceled', 'simply-schedule-appointments' ) . ': ' . $title;
		}

		if ( $this->is_group_event() && $this->is_group_canceled() ) {
			$title = __('Canceled', 'simply-schedule-appointments' ) . ': ' . $title;
		}

		return $title;
	}

	/**
	 * Given a specific recipient type, get's the right twig template to
	 * populate the event location, and parses the content.
	 *
	 * @since 5.4.0
	 *
	 * @param SSA_Recipient $recipient The recipient object.
	 * @return string the event location.
	 */
	public function get_calendar_event_location( SSA_Recipient $recipient ) {
		$event_type     = $this->get_calendar_event_type( $recipient );
		$calendar_event = new SSA_Calendar_Events_Object( $event_type );

		$location = $calendar_event->get_calendar_event_content( 'location', $this->id, true );

		return $location;
	}

	/**
	 * Given a specific recipient type, get's the right twig template to
	 * populate the event description, and parses the content.
	 *
	 * @since 5.4.0
	 *
	 * @param SSA_Recipient $recipient The recipient object.
	 * @return string the event description.
	 */
	public function get_calendar_event_description( SSA_Recipient $recipient ) {
		$event_type     = $this->get_calendar_event_type( $recipient );
		$calendar_event = new SSA_Calendar_Events_Object( $event_type );

		$description = $calendar_event->get_calendar_event_content( 'details', $this->id );

		$clear_map = array(
			'</p>'   => "</p>\r\n",
			'<br />' => "\r\n",
			'<br>'   => "\r\n",
			'<br/>'  => "\r\n",
		);

		// Remove all html tags and turn paragraphs or <br> into line breaks.
		// Required to avoid html issues with Google Calendar and other calendar apps.
		$description = str_replace( array_keys( $clear_map ), array_values( $clear_map ), $description );
		$description = strip_tags( $description, '<a>' );

		return $description;
	}

	/**
	 * Returns the event title for a specific template.
	 *
	 * @since 5.4.0
	 *
	 * @param string $template The template name.
	 * @return string The event title.
	 */
	public function get_title( $template ) {
		if ( 'customer' === $template ) {
			$recipient = SSA_Recipient_Customer::create();
		}
		if ('staff' === $template ) {
			$recipient = SSA_Recipient_Staff::create();
		}

		return $this->get_calendar_event_title( $recipient );
	}

	/**
	 * Returns the event location for a specific template.
	 *
	 * @since 5.4.0
	 *
	 * @param string $template The template name.
	 * @return string The event location.
	 */
	public function get_location( $template ) {
		if ( 'customer' === $template ) {
			$recipient = SSA_Recipient_Customer::create();
		}
		if ( 'staff' === $template ) {
			$recipient = SSA_Recipient_Staff::create();
		}
		return $this->get_calendar_event_location( $recipient );
	}

	public function get_description( $template, $eol = "\r\n" ) {
		if ( 'customer' === $template ) {
			$recipient = SSA_Recipient_Customer::create();
		}
		if ( 'staff' === $template ) {
			$recipient = SSA_Recipient_Staff::create();
		}

		return $this->get_calendar_event_description( $recipient );
	}

	/**
	 * Returns a list of attendees for the this appointment.
	 *
	 * @since 5.6.0
	 *
	 * @return array The list of attendees.
	 */
	public function get_attendees() {
		$attendees = array();

		// Add customer if it's a shared calendar event.
		$shared_calendar_event = $this->get_appointment_type()->shared_calendar_event;

		if(!empty($shared_calendar_event)){
			$new_attendee = array(
				'email' => $this->get_customer_email(),
				'name'  => $this->get_customer_name(),
			);
			if ( ! empty( $new_attendee['email'] ) && ! empty( $new_attendee['name'] ) ) {
				// avoids edge cases with gravity/formidable forms that don't have a proper name/email field
				$attendees[] = $new_attendee;
			}
		}

		// Add team members if any is set to the appointment.
		$staff_ids = ssa()->staff_appointment_model->get_staff_ids( $this->id );
		if ( ! empty( $staff_ids ) ) {
			foreach ( $staff_ids as $staff_id ) {
				$staff = new SSA_Staff_Object( $staff_id );

				$attendees[] = array(
					'email' => $staff->email,
					'name'  => $staff->display_name,
				);
			}
		}

		/**
		 * Filters the list of attendees for an appointment.
		 *
		 * @since 5.6.0
		 *
		 * @param array $attendees The list of attendees.
		 * @param int  $appointment_id The appointment ID.
		 */
		$attendees = apply_filters( 'ssa/appointment/attendees', $attendees, $this->id ); //@codingStandardsIgnoreLine (WordPress doesn't like hook names with slashes).

		return $attendees;
	}

	public function get_calendar_id() {
		$group = $this->get_group_appointment();
		if ( ! empty( $group ) ) {
			return $group->__get( 'google_calendar_id' );
		}

		return $this->__get( 'google_calendar_id' );
	}

	public function get_calendar_event_id() {
		$group = $this->get_group_appointment();
		if ( ! empty( $group ) ) {
			return $group->google_calendar_event_id;
		}

		return $this->__get( 'google_calendar_event_id' );
	}

	/**
	 * Set an SSA_Ics_Exporter instance and define the template.
	 *
	 * @param string $template The template name.
	 * @return SSA_Ics_Exporter
	 */
	public function get_ics_exporter( $template = 'customer' ) {
		$ics_exporter           = new SSA_Ics_Exporter();
		$ics_exporter->template = $template;

		return $ics_exporter;
	}

	/**
	 * Get .ics file contents and headers.
	 *
	 * @param string $template The template name.
	 * @return array The .ics file contents and headers.
	 */
	public function get_ics( $template = 'customer' ) {
		$ics_exporter = $this->get_ics_exporter( $template );
		$ics          = $ics_exporter->get_ics_for_appointment( $this );

		return $ics;
	}

	/**
	 * Return the download url for the .ics file.
	 *
	 * @since 5.4.4
	 *
	 * @param string $type The user type.
	 *
	 * @return string The download url.
	 */
	public function get_ics_download_url( $type = 'customer' ) {
		// Get the rest api root url.
		$base = ssa()->appointment_model->get_ics_endpoints_base();
		$url  = $base . $this->id . '/ics/download/' . $type;

		return $url;
	}

	public function get_gcal_add_link( $template = 'customer' ) {
		$link = ssa()->gcal_exporter->get_add_link_from_appointment( $this, $template );

		return $link;
	}


	public function is_all_day() {
		return false;
	}

	public function get_public_edit_url() {
		$url = ssa()->appointment_model->get_public_edit_url( $this->id );
		return $url;
	}
	public function get_admin_edit_url() {
		$url = ssa()->appointment_model->get_admin_edit_url( $this->id );
		return $url;
	}

	public function is_unavailable() {
		return in_array( $this->__get( 'status' ), SSA_Appointment_Model::get_unavailable_statuses() );
	}
	public function is_available() {
		return ! $this->is_unavailable();
	}
	public function is_reserved() {
		return in_array( $this->__get( 'status' ), SSA_Appointment_Model::get_reserved_statuses() );
	}
	public function is_booked() {
		return in_array( $this->__get( 'status' ), SSA_Appointment_Model::get_booked_statuses() );
	}
	public function is_canceled() {
		return in_array( $this->__get( 'status' ), SSA_Appointment_Model::get_canceled_statuses() );
	}
	public function is_group_canceled() {
		if ( ! $this->is_group_event() ) {
			return null;
		}

		$group_id = $this->__get( 'group_id' );
		if ( empty( $group_id ) ) {
			return false;
		}

		$appointment_arrays = ssa()->appointment_model->query( array(
			'number' => -1,
			'group_id' => $group_id,
		) );
		if ( empty( $appointment_arrays ) ) {
			return false;
		}

		$is_group_canceled = true;
		foreach ($appointment_arrays as $appointment_array) {
			if ( in_array( $appointment_array['status'], SSA_Appointment_Model::get_booked_statuses() ) ) {
				return false;
			}
		}

		return $is_group_canceled;
	}

	public function get_group_appointment() {
		if ( ! $this->is_group_event() ) {
			return;
		}

		$group_id = $this->__get( 'group_id' );
		if ( empty( $group_id ) ) {
			return;
		}

		$group = new SSA_Appointment_Object( $group_id );
		return $group;
	}

	public function query_group_appointments() {
		if ( ! $this->is_group_event() ) {
			return;
		}

		$group_id = $this->__get( 'group_id' );
		if ( empty( $group_id ) ) {
			return;
		}

		$groups = ssa()->appointment_model->query( array(
			'number' => -1,
			'group_id' => $group_id,
		) );

		$group_objects = array();
		foreach ($groups as $group) {
			$group_objects[] = new SSA_Appointment_Object( $group['id'] );
		}

		return $group_objects;
	}

	public function is_group_event() {
		$capacity_type = $this->get_appointment_type()->capacity_type;
		return ( $capacity_type === 'group' );
	}

	public function is_group_parent() {
		if ( ! $this->is_group_event() ) {
			return false;
		}
		
		$group_id = $this->__get( 'group_id' );
		if ( $group_id == $this->id ) {
			return true;
		}

		return false;
	}

	public function is_individual_appointment() {
		$capacity_type = $this->get_appointment_type()->capacity_type;
		return ( $capacity_type === 'individual' );
	}

	/**
	 * Given a specific recipient type, returns the calendar event type slug.
	 *
	 * @since 5.4.0
	 *
	 * @param SSA_Recipient_Admin|SSA_Recipient_Customer|SSA_Recipient_Shared|SSA_Recipient_Staff $recipient the recipient class.
	 * @return string|boolean the event type slug.
	 */
	public function get_calendar_event_type( $recipient ) {
		if (
			! $recipient instanceof SSA_Recipient_Admin &&
			! $recipient instanceof SSA_Recipient_Customer &&
			! $recipient instanceof SSA_Recipient_Shared &&
			! $recipient instanceof SSA_Recipient_Staff
		) {
			return null;
		}

		if ( $recipient->is_customer() && $recipient->is_business() ) {
			if ( $this->is_group_event() ) {
				return 'group_shared';
			} elseif ( $this->is_individual_appointment() ) {
				return 'individual_shared';
			}
		} elseif ( $recipient->is_customer() ) {
			return 'customer';
		} elseif ( $recipient->is_business() ) {
			if ( $this->is_group_event() ) {
				return 'group_admin';
			} elseif ( $this->is_individual_appointment() ) {
				return 'individual_admin';
			}
		}
	}

	public function format_webhook_payload( $payload ) {
		$payload['appointment'] = shortcode_atts( array(
			'id' => '',
			'appointment_type_id' => '',
			'appointment_type_slug' => '',
			'customer_id' => '',
			'customer_information' => array(),
			'post_information'  => array(),
			'customer_timezone' => '',
			'start_date' => '',
			'end_date' => '',
			'status' => '',
			'date_created' => '',
			'date_modified' => '',
			'public_edit_url' => '',
			'price_full' => '',
			'payment_received' => '',
			'payment_method' => '',
			'web_meeting_url' => '',
			'web_meeting_password' => '',
		), $payload['appointment'] );

		$dates_to_localize = array(
			'start_date',
			'end_date',
			'date_created',
			'date_modified',
		);

		if ( empty( $payload['appointment']['appointment_type_slug'] ) && !empty( $payload['appointment']['appointment_type_id'] ) ) {
			$appointment_type_object = $this->get_appointment_type();
			$payload['appointment']['appointment_type_slug'] = $appointment_type_object->slug;
		}

		if ( empty( $payload['appointment']['appointment_type_title'] ) && !empty( $payload['appointment']['appointment_type_id'] ) ) {
			$appointment_type_object = $this->get_appointment_type();
			$payload['appointment']['appointment_type_title'] = $appointment_type_object->title;
		}

		// price_full is not a column on modern installs (dropped in #652) but a
		// residual DECIMAL(9,2) column still exists on pre-Mar-2023 DBs, where
		// SELECT * leaks a stale "0.00". Treat 0/""/null all as "no price set"
		// and backfill from the appointment type's configured price.
		if ( empty( (float) $payload['appointment']['price_full'] ) && ! empty( $payload['appointment']['appointment_type_id'] ) ) {
			$appointment_type_object = $this->get_appointment_type();
			if ( ! empty( $appointment_type_object->payments['price'] ) ) {
				$payload['appointment']['price_full'] = (float) $appointment_type_object->payments['price'];
			}
		}

		$settings_global = ssa()->settings->get()['global'];
		foreach ( $dates_to_localize as $key ) {
			if ( empty( $payload['appointment'][$key] ) ) {
				continue;
			}

			/* Raw */
			$payload['appointment']['local_time_for']['appointment_type']['raw'][$key] = ssa()->utils->get_datetime_as_local_datetime( $payload['appointment'][$key], $payload['appointment']['appointment_type_id'] )->format( 'Y-m-d H:i:s' );
			$payload['appointment']['local_time_for']['appointment_type']['raw_parts'][$key]['date'] = ssa()->utils->get_datetime_as_local_datetime( $payload['appointment'][$key], $payload['appointment']['appointment_type_id'] )->format( 'Y-m-d' );
			$payload['appointment']['local_time_for']['appointment_type']['raw_parts'][$key]['time'] = ssa()->utils->get_datetime_as_local_datetime( $payload['appointment'][$key], $payload['appointment']['appointment_type_id'] )->format( 'H:i:s' );
			$payload['appointment']['local_time_for']['appointment_type']['raw_parts'][$key]['timezone'] = ssa()->utils->get_datetime_as_local_datetime( $payload['appointment'][$key], $payload['appointment']['appointment_type_id'] )->format( 'e' );
			$payload['appointment']['local_time_for']['appointment_type']['raw_parts'][$key]['timezone_offset'] = ssa()->utils->get_datetime_as_local_datetime( $payload['appointment'][$key], $payload['appointment']['appointment_type_id'] )->format( 'O' );


			/* Formatted */
			$payload['appointment']['local_time_for']['appointment_type']['formatted'][$key] = ssa()->utils->get_datetime_as_local_datetime( $payload['appointment'][$key], $payload['appointment']['appointment_type_id'] )->format( $settings_global['date_format'].' '.$settings_global['time_format']. ' T' );

			$payload['appointment']['local_time_for']['appointment_type']['formatted_parts'][$key]['date'] = ssa()->utils->get_datetime_as_local_datetime( $payload['appointment'][$key], $payload['appointment']['appointment_type_id'] )->format( $settings_global['date_format'] );
			$payload['appointment']['local_time_for']['appointment_type']['formatted_parts'][$key]['time'] = ssa()->utils->get_datetime_as_local_datetime( $payload['appointment'][$key], $payload['appointment']['appointment_type_id'] )->format( $settings_global['time_format'] );
			$payload['appointment']['local_time_for']['appointment_type']['formatted_parts'][$key]['timezone'] = ssa()->utils->get_datetime_as_local_datetime( $payload['appointment'][$key], $payload['appointment']['appointment_type_id'] )->format( 'T' );
		}

		if( ! empty( $payload['meta']['booking_url'] ) ) {
			$payload['post_information']['booking_url'] = $payload['meta']['booking_url'];
		}
		if( ! empty( $payload['meta']['booking_title'] ) ) {
			$payload['post_information']['booking_title'] = $payload['meta']['booking_title'];
		}
		if( ! empty( $payload['meta']['booking_post_id'] ) ) {
			$payload['post_information']['booking_post_id'] = $payload['meta']['booking_post_id'];
		}

		// Memberpress integration
		if( ! empty( $payload['meta']['mepr_membership_id'] ) && class_exists( 'SSA_Mepr_Membership' ) && class_exists( 'MeprProduct' ) ) {
			$membership = new SSA_Mepr_Membership( $payload['meta']['mepr_membership_id'] );
			if ( $membership->exists() ) {
				$payload['mepr_membership'] = array(
					'id'    => $membership->get_product_id(),
					'title' => $membership->get_title()
				);
			}
			unset( $payload['meta']['mepr_membership_id'] );
		}

		return $payload;
	}

	public function get_webhook_payload( $action ) {
		$action_noun = '';
		$action_verb = '';
		if ( false !== strpos( $action, '_' ) ) {
			$action_noun = explode( '_', $action )[0];
			$action_verb = explode( '_', $action )[1];
		}
		$payload = array(
			'action' => $action,
			'action_noun' => $action_noun,
			'action_verb' => $action_verb,
			'appointment' => $this->get_data( 0 ),
			'team_members'=> $this->get_staff_members_to_webhook(),
			'meta'        => ssa()->appointment_model->get_metas( $this->id ),
		);
		$payload = $this->format_webhook_payload( $payload );

		return $payload;
	}
	
	public function get_staff_members_to_webhook() {

		$staff = $this->get_staff_members();

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

		$output =  [];
		for ($i=0 ; $i<count($staff) ; $i++) {
			$output[] = [
				'id'			=> $staff[$i]->id,
				'wp_user_id'	=> $staff[$i]->user_id,
				'name'			=> $staff[$i]->name ? $staff[$i]->name : $staff[$i]->get_name(),
				'display_name'	=> $staff[$i]->get_name(),
				'email'			=> $staff[$i]->get_email(),
			];
		}
		return $output;
	}

	public function cancel( $metas=array() ) {
		if ( $this->is_canceled() ) return;
		
		$this->status = 'canceled';
		ssa()->appointment_model->update( $this->id, array(
				'status' => $this->status,
		));

		if ( ! empty( $metas['cancelation_note'] ) ) {
			$meta_keys_and_values = array();
			$meta_keys_and_values['cancelation_note'] = esc_attr( trim( $metas['cancelation_note'] ) );
			ssa()->appointment_meta_model->bulk_meta_update( $this->id, $meta_keys_and_values );
		}

	}


	/**
	 * Watch out! This method does not check if the appointment type has the opt-in notifications enabled.
	 */
	public function customer_has_not_opted_in() {
		$meta = ssa()->appointment_model->get_metas( $this->id );
		return empty( $meta['opt_in_notifications'] );
	}

}
