<?php
/**
 * Simply Schedule Appointment Revision Model.
 *
 * @since   6.1.0
 * @package Simply_Schedule_Appointments
 */

/**
 * Simply Schedule Appointment Revision Model.
 *
 * @since 6.1.0
 */
class SSA_Revision_Model extends SSA_Db_Model {
	protected $slug    = 'revision';
	protected $version = '3.7.6';
	/**
	 * Parent plugin class.
	 *
	 * @since 6.1.0
	 *
	 * @var   Simply_Schedule_Appointments
	 */
	protected $plugin = null;

	/**
	 * Constructor.
	 *
	 * @since  6.1.0
	 *
	 * @param  Simply_Schedule_Appointments $plugin Main plugin object.
	 */
	public function __construct( $plugin ) {

		parent::__construct( $plugin );

		$this->hooks();
	}

	/**
	 * Initiate our hooks.
	 *
	 * @since  6.1.0
	 */
	public function hooks() {
		add_action( 'ssa/appointment/booked', array( $this, 'insert_revision_booked_appointment' ), 10, 3 );
		add_action( 'ssa/appointment/edited', array( $this, 'insert_revision_edited_appointment' ), 10, 3 );
		add_action( 'ssa/appointment/rescheduled', array( $this, 'insert_revision_rescheduled_appointment' ), 10, 3 );
		add_action( 'ssa/appointment/abandoned', array( $this, 'insert_revision_abandoned_appointment' ), 10, 3 );
		add_action( 'ssa/appointment/canceled', array( $this, 'insert_revision_canceled_appointment' ), 10, 3 );
		add_action( 'ssa/appointment/pending', array( $this, 'insert_revision_pending_appointment' ), 10, 3 );

		// Add revision on appointment no show/reverted
		add_action( 'ssa/appointment/no_show', array( $this, 'insert_revision_no_show_appointment' ), 10, 3 );
		add_action( 'ssa/appointment/no_show_reverted', array( $this, 'insert_revision_no_show_reverted_appointment' ), 10, 3 );

		// Add revision first assigned to team member 
		add_action( 'ssa/appointment/after_insert', array( $this, 'maybe_insert_revision_assigned_appointment' ), 1000, 2 );

		// Add revision record whenever an appointment is reassigned
		add_action( 'ssa/appointment/after_update', array( $this, 'maybe_insert_revision_reassigned_appointment' ), 1000, 3 );


		// scheduled cleanup
		add_action( 'init', array( $this, 'schedule_async_actions' ) );
		add_action( 'ssa/revisions/cleanup', array( $this, 'cleanup_revisions' ), 10, 0 );

		// Notifications
		add_action( 'ssa/notification/scheduled', array( $this, 'insert_revision_on_notification_scheduled' ), 10, 9 );
		add_action( 'ssa/notification/sent', array( $this, 'insert_revision_on_notification_sent' ), 10, 8 );

		//Appointment Types revisions
		add_action( 'ssa/appointment_type/after_insert', array( $this, 'insert_revision_created_appointment_type' ), 1000, 3 );
		add_action( 'ssa/appointment_type/after_delete', array( $this, 'insert_revision_deleted_appointment_type' ), 1000, 3 );
		add_action( 'ssa/appointment_type/after_update', array( $this, 'insert_revision_updated_appointment_type' ), 1000, 3 );
	}

	/**
	 * Scheduling the revisions cleanup async action
	 *
	 * @return void
	 */
	public function schedule_async_actions() {
		if( ssa_should_skip_async_logic() ) {
			return;
		}
		
		// below functions wrap the action scheduler methods, make all the needed checks and log any failures
		if ( false === ssa_has_scheduled_action( 'ssa/revisions/cleanup' ) ) {
			ssa_schedule_recurring_action( strtotime( 'now' ), DAY_IN_SECONDS, 'ssa/revisions/cleanup' );
		}
	}

	/**
	 * revisions scheduled cleanup
	 *
	 * @return void
	 */
	public function cleanup_revisions() {
		$revisions = $this->query(
			array(
				'date_created_max' => date( 'Y-m-d H:i:s', strtotime( '-3 months' ) ),
			)
		);

		// get ids of revisions as an array
		$revisions_ids = wp_list_pluck( $revisions, 'id' );
		if ( ! empty( $revisions_ids ) ) {
			// delete all corresponding revision_meta rows
			$this->plugin->revision_meta_model->bulk_delete(
				array(
					'revision_id' => $revisions_ids,
				)
			);
			// delete revisions rows
			$this->bulk_delete(
				array(
					'id' => $revisions_ids,
				) );
		}

		$this->check_orphaned_revisions_meta();
	}

	/**
	 * Check for orphaned revisions meta, that their revision_id doesn't exist and delete them
	 * TODO: Consider removing this function in the future
	 *
	 * @return void
	 */
	public function check_orphaned_revisions_meta() {
		// make sure to query all the revisions entries
		$revisions = $this->query(
			array(
				'number'  => -1,
				'orderby' => 'id',
				'order'   => 'ASC', 
				'fields'  => ['id']
			)
		);

		if ( empty( $revisions ) ) {
			return; // This should be a new site with no appointments yet.
		}

		$revisions = wp_list_pluck( $revisions, 'id', 'id' );

		$revision_meta = $this->plugin->revision_meta_model->query(
			array(
				'number'  => 50,
				'orderby' => 'id',
				'order'   => 'ASC', 
			)
		);

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

		$revision_meta = wp_list_pluck( $revision_meta, 'id', 'revision_id' );

		$revisons_ids_to_delete = array();
		foreach ( $revision_meta as $revison_id => $id ) {
			if ( ! isset( $revisions[ $revison_id ] ) ) {
				$revisons_ids_to_delete[] = $revison_id;
			}
		}

		if ( ! empty( $revisons_ids_to_delete ) ) {
			$this->plugin->revision_meta_model->bulk_delete(
				array(
					'revision_id' => $revisons_ids_to_delete,
				)
			);
		}
	}

	public function has_many() {
		return array(
			// TODO check correct name
			'Revision_Meta_Values' => array(
				'model'       => $this->plugin->revision_meta_model,
				'foreign_key' => 'revision_id',
			),
		);
	}

	protected $schema = array(
		'result'              => array(
			'field'            => 'result',
			'label'            => 'Result',
			'default_value'    => '',
			'format'           => '%s',
			'mysql_type'       => 'VARCHAR', // 'success', 'failure', or 'warning'
			'mysql_length'     => '8',
			'mysql_unsigned'   => false,
			'mysql_allow_null' => false,
			'mysql_extra'      => '',
			'cache_key'        => false,
		),
		// foreign key
		'appointment_id'      => array(
			'field'            => 'appointment_id',
			'label'            => 'Appointment ID',
			'default_value'    => 0,
			'format'           => '%d',
			'mysql_type'       => 'BIGINT',
			'mysql_length'     => 20,
			'mysql_unsigned'   => true,
			'mysql_allow_null' => false,
			'mysql_extra'      => '',
			'cache_key'        => false,
		),

		// foreign key
		'appointment_type_id' => array(
			'field'            => 'appointment_type_id',
			'label'            => 'Appointment Type ID',
			'default_value'    => 0,
			'format'           => '%d',
			'mysql_type'       => 'BIGINT',
			'mysql_length'     => 20,
			'mysql_unsigned'   => true,
			'mysql_allow_null' => false,
			'mysql_extra'      => '',
			'cache_key'        => false,
		),

		// foreign key
		'user_id'             => array(
			'field'            => 'user_id',
			'label'            => 'Customer ID',
			'default_value'    => 0,
			'format'           => '%d',
			'mysql_type'       => 'BIGINT',
			'mysql_length'     => 20,
			'mysql_unsigned'   => true,
			'mysql_allow_null' => false,
			'mysql_extra'      => '',
			'cache_key'        => false,
		),

		// foreign key
		'staff_id'            => array(
			'field'            => 'staff_id',
			'label'            => 'Staff ID',
			'default_value'    => 0,
			'format'           => '%d',
			'mysql_type'       => 'BIGINT',
			'mysql_length'     => 20,
			'mysql_unsigned'   => true,
			'mysql_allow_null' => false,
			'mysql_extra'      => '',
			'cache_key'        => false,
		),

		// foreign key
		'payment_id'          => array(
			'field'            => 'payment_id',
			'label'            => 'Payment ID',
			'default_value'    => 0,
			'format'           => '%d',
			'mysql_type'       => 'BIGINT',
			'mysql_length'     => 20,
			'mysql_unsigned'   => true,
			'mysql_allow_null' => false,
			'mysql_extra'      => '',
			'cache_key'        => false,
		),

		// foreign key
		'async_action_id'     => array(
			'field'            => 'async_action_id',
			'label'            => 'Async Action ID',
			'default_value'    => 0,
			'format'           => '%d',
			'mysql_type'       => 'BIGINT',
			'mysql_length'     => 20,
			'mysql_unsigned'   => true,
			'mysql_allow_null' => false,
			'mysql_extra'      => '',
			'cache_key'        => false,
		),

		// allows filtering/looking at what happened in a certain timeframe
		'date_created'        => array(
			'field'            => 'date_created',
			'label'            => 'Date Created',
			'default_value'    => false,
			'format'           => '%s',
			'mysql_type'       => 'DATETIME',
			'mysql_length'     => '',
			'mysql_unsigned'   => false,
			'mysql_allow_null' => true,
			'mysql_extra'      => '',
			'cache_key'        => false,
		),

		// action ( edit, cancel, etc...)
		'action'              => array(
			'field'            => 'action',
			'label'            => 'Action',
			'default_value'    => '',
			'format'           => '%s',
			'mysql_type'       => 'VARCHAR',
			'mysql_length'     => '32',
			'mysql_unsigned'   => false,
			'mysql_allow_null' => false,
			'mysql_extra'      => '',
			'cache_key'        => false,
		),

		// action_title ( Appointment Canceled, Appointment Booked, etc...)
		'action_title'				=> array(
			'field'            => 'action_title',
			'label'            => 'Action Title',
			'default_value'    => '',
			'format'           => '%s',
			'mysql_type'       => 'TINYTEXT',
			'mysql_length'     => false,
			'mysql_unsigned'   => false,
			'mysql_allow_null' => false,
			'mysql_extra'      => '',
			'cache_key'        => false,
		),

		// event summary
		'action_summary'      => array(
			'field'            => 'action_summary',
			'label'            => 'Action Summary',
			'default_value'    => '',
			'format'           => '%s',
			'mysql_type'       => 'TEXT',
			'mysql_length'     => false,
			'mysql_unsigned'   => false,
			'mysql_allow_null' => false,
			'mysql_extra'      => '',
			'cache_key'        => false,
		),

		'summary_vars'        => array(
			'field'            => 'summary_vars',
			'label'            => 'Summary Variables',
			'default_value'    => '',
			'format'           => '%s',
			'mysql_type'       => 'TEXT',
			'mysql_length'     => false,
			'mysql_unsigned'   => false,
			'mysql_allow_null' => false,
			'mysql_extra'      => '',
			'cache_key'        => false,
			'encoder'          => 'json_serialize',
		),

		// context ( booking, settings, syncing etc...)
		'context'             => array(
			'field'            => 'context',
			'label'            => 'Context',
			'default_value'    => '',
			'format'           => '%s',
			'mysql_type'       => 'VARCHAR',
			'mysql_length'     => '32',
			'mysql_unsigned'   => false,
			'mysql_allow_null' => false,
			'mysql_extra'      => '',
			'cache_key'        => false,
		),

		// better filtering, also cross context filtering, like web meetings across several contexts (google, zoom,, etc)
		'sub_context'         => array(
			'field'            => 'sub_context',
			'label'            => 'Sub Context',
			'default_value'    => '',
			'format'           => '%s',
			'mysql_type'       => 'VARCHAR',
			'mysql_length'     => '32',
			'mysql_unsigned'   => false,
			'mysql_allow_null' => false,
			'mysql_extra'      => '',
			'cache_key'        => false,
		),

		'stack_trace' => array(
			'field'            => 'stack_trace',
			'label'            => 'Stack Trace',
			'default_value'    => '',
			'format'           => '%s',
			'mysql_type'       => 'TEXT', 
			'mysql_length'     => false,
			'mysql_unsigned'   => false,
			'mysql_allow_null' => true,
			'mysql_extra'      => '', 
			'cache_key'        => false, 
		),
	);


	// below fields are indexed to use in filtering, like getting all events of a certain appointment_id
	public $indexes = array(
		'appointment_id'      => array( 'appointment_id' ),
		'appointment_type_id' => array( 'appointment_type_id' ),
		'user_id'             => array( 'user_id' ),
		'staff_id'            => array( 'staff_id' ),
		'async_action_id'     => array( 'async_action_id' ),
		'date_created'        => array( 'date_created' ),
		'action'              => array( 'action' ),
		'context'             => array( 'context' ),
		'sub_context'         => array( 'sub_context' ),
	);


	public function get_items_permissions_check( $request ) {
		
		if ( current_user_can( 'ssa_manage_others_appointments' ) ) {
			return true;
		}

		if ( current_user_can( 'ssa_manage_appointments' ) ) {
			return true;
		}

		if (current_user_can( 'ssa_manage_appointment_types' ) ) {
			return true;
		}

		if ( true === parent::get_item_permissions_check( $request ) ) {
			return true;
		}
		
		return false;
	}

	// TODO IMPORTANT: if each action only has one foreign id populated, 2 or more of the below where conditions will eliminate every result
	public function filter_where_conditions( $where, $args ) {
		global $wpdb;
		if ( ! empty( $args['appointment_id'] ) ) {
			if( is_array( $args['appointment_id'] ) ) {
				$appointment_ids = implode( ',', array_map('intval', $args['appointment_id'] ) );
			} else {
				$appointment_ids = intval( $args['appointment_id'] );
			}
			$where .= " AND `appointment_id` IN( $appointment_ids ) ";
		}

		if ( ! empty( $args['appointment_type_id'] ) ) {
			$where .=  $wpdb->prepare( ' AND appointment_type_id=%d', sanitize_text_field( $args['appointment_type_id'] ) );
		}

		if ( ! empty( $args['user_id'] ) ) {
			$where .=  $wpdb->prepare( ' AND user_id=%d', sanitize_text_field( $args['user_id'] ) );
		}

		if ( ! empty( $args['staff_id'] ) ) {
			$where .=  $wpdb->prepare( ' AND staff_id=%d', sanitize_text_field( $args['staff_id'] ) );
		}

		if ( ! empty( $args['async_action_id'] ) ) {
			$where .=  $wpdb->prepare( ' AND async_action_id=%d', sanitize_text_field( $args['async_action_id'] ) );
		}

		if ( ! empty( $args['date_created'] ) ) {
			$where .=  $wpdb->prepare( ' AND date_created=%s', sanitize_text_field( $args['date_created'] ) );
		}

		if ( ! empty( $args['action'] ) ) {
			$where .=  $wpdb->prepare( ' AND action=%s', sanitize_text_field( $args['action'] ) );
		}

		if ( ! empty( $args['context'] ) ) {
			$where .=  $wpdb->prepare( ' AND context=%s', sanitize_text_field( $args['context'] ) );
		}

		if ( ! empty( $args['sub_context'] ) ) {
			$where .=  $wpdb->prepare( ' AND sub_context=%s', sanitize_text_field( $args['sub_context'] ) );
		}

		return $where;

	}

	// =================================================================================
	//
	// Section: Revision insertion function definitions
	//
	// Pattern: functions restructure the data available then pass it to insert_revision
	// ==================================================================================

	/**
	 * When it's first assigned to a team member
	 *
	 * @param integer $appointment_id
	 * @param array $data
	 * @return void
	 */
	public function maybe_insert_revision_assigned_appointment( $appointment_id, $data_after, $data_before = null ) {

		if ( empty( $data_after['staff_ids'] ) || ! empty( $data_before ) ) return;

		$params = array(
			'result'				 => 'success',
			'action'				 => 'assigned',
			'appointment_id' => $appointment_id,
			'data_after' 		 => $data_after,
			'data_before'		 => $data_before,
			'staff_ids'			 => $data_after['staff_ids']
		);
		$this->insert_revision_appointment( $params );
	}

	public function maybe_insert_revision_reassigned_appointment( $appointment_id, $data_after, $data_before ) {

		if ( SSA_Appointment_Model::is_appointment_reassigned( $data_after, $data_before ) ) {
			$params = array(
				'result'				 => 'success',
				'action'				 => 'reassigned',
				'appointment_id' => $appointment_id,
				'data_after' 		 => $data_after,
				'data_before'		 => $data_before,
				'staff_ids'			 => $data_after['staff_ids']
			);
			$this->insert_revision_appointment( $params );
		}

	}

	public function insert_revision_gcal_after_sync( $result, $appointment_id, $action, $action_summary, $calendar_id, $calendar_event_id, $event = null ) {
		$revision_meta = array(
				'calendar_id' => $calendar_id,
				'calendar_event_id' => $calendar_event_id,
			);

		if ( ! empty( $event ) ) {
			$revision_meta['event'] = (array) $event;
		}

		// below: hints for WP.org to pick up phrases for translation
		// __( 'Could not find existing event details for appointment ID {{ appointment_id }}', 'simply-schedule-appointments' );
		// __( 'Error while creating event for appointment ID {{ appointment_id }}', 'simply-schedule-appointments' );
		// __( 'Deleted group event for appointment ID {{ appointment_id }}', 'simply-schedule-appointments' );
		// __( 'Deleted individual event for appointment ID {{ appointment_id }}', 'simply-schedule-appointments' );
		// __( 'Exception occured while doing sync for appointment ID {{ appointment_id }}', 'simply-schedule-appointments' );
		// __( 'Inserted GCAL event for appointment ID {{ appointment_id }}', 'simply-schedule-appointments' );
		// __( 'Updated GCAL event for appointment ID {{ appointment_id }}', 'simply-schedule-appointments' );

		$this->insert_revision(
			array(
				'result'         => $result,
				'appointment_id' => $appointment_id,
				'action'         => $action,
				'action_title'	 => $this->get_action_title( $action, $result ),
				'action_summary' => $action_summary,
				'summary_vars'   => array(),
				'context'        => 'gcal',
				'sub_context'    => 'sync',
			),
			$revision_meta
		);
	}

	// insert revisions opt out notification
	public function insert_revision_opt_out_notification( $appointment_id, $data_after ) {
		$params = array(
			'result'				 => 'success',
			'action'				 => 'opt_out_notification',
			'appointment_id' => $appointment_id,
			'data_after' 		 => $data_after,
			'data_before'		 => array(),
		);
		$this->insert_revision_appointment( $params );
	}

	public function insert_revision_abandoned_appointment( $appointment_id, $data_after, $data_before = null ) {
		$params = array(
			'result'				 => 'success',
			'action'				 => 'abandoned',
			'appointment_id' => $appointment_id,
			'data_after' 		 => $data_after,
			'data_before'		 => $data_before
		);
		$this->insert_revision_appointment( $params );
	}

	public function insert_revision_edited_appointment( $appointment_id, $data_after, $data_before = null ) {
		$params = array(
			'result'				 => 'success',
			'action'				 => 'edited',
			'appointment_id' => $appointment_id,
			'data_after' 		 => $data_after,
			'data_before'		 => $data_before
		);
		$this->insert_revision_appointment( $params );
	}

	public function insert_revision_rescheduled_appointment( $appointment_id, $data_after, $data_before = null ) {
		// Formatting the appointment date and time
		$date_format = SSA_Utils::localize_default_date_strings( 'F j, Y' );
		$time_format = SSA_Utils::localize_default_date_strings( 'g:i a' );
		
		$business_previous_start_date =  $this->plugin->utils->get_datetime_as_local_datetime( $data_before['start_date'] )->format( $date_format );
		$business_previous_start_time =  $this->plugin->utils->get_datetime_as_local_datetime( $data_before['start_date'] )->format( $time_format );
		
		$business_start_date = $this->plugin->utils->get_datetime_as_local_datetime( $data_after['start_date'] )->format( $date_format );
		$business_start_time = $this->plugin->utils->get_datetime_as_local_datetime( $data_after['start_date'] )->format( $time_format );
		
		$params = array(
			'result'				 => 'success',
			'action'				 => 'rescheduled',
			'appointment_id' => $appointment_id,
			'data_after' 		 => $data_after,
			'data_before'		 => $data_before,
			'business_previous_start_date' => $business_previous_start_date,
			'business_previous_start_time' => $business_previous_start_time,
			'business_start_date' => $business_start_date,
			'business_start_time' => $business_start_time,
		);
		$this->insert_revision_appointment( $params );
	}
	
	public function insert_revision_booked_appointment( $appointment_id, $data_after, $data_before = null ) {
		$action = empty( $data_before ) ? 'first_booked' : 'booked';
		
		$params = array(
			'result'				 => 'success',
			'action'				 => $action,
			'appointment_id' => $appointment_id,
			'data_after' 		 => $data_after,
			'data_before'		 => $data_before
		);
		$this->insert_revision_appointment( $params );
	}

	public function insert_revision_canceled_appointment( $appointment_id, $data_after, $data_before = null ) {
		$params = array(
			'result'				 => 'success',
			'action'				 => 'canceled',
			'appointment_id' => $appointment_id,
			'data_after' 		 => $data_after,
			'data_before'		 => $data_before
		);
		$this->insert_revision_appointment( $params );
	}

	public function insert_revision_pending_appointment( $appointment_id, $data_after, $data_before = null ) {
		$params = array(
			'result'				 => 'success',
			'action'				 => $data_after['status'], // pending_form or pending_payment
			'appointment_id' => $appointment_id,
			'data_after' 		 => $data_after,
			'data_before'		 => null
		);
		$this->insert_revision_appointment( $params );
	}

	public function insert_revision_no_show_appointment( $appointment_id, $data_after, $meta_data ) {
		$params = array(
			'result'				 => 'success',
			'action'				 => 'no_show',
			'appointment_id' => $appointment_id,
			'data_after' 		 => $data_after,
			'data_before'		 => null
		);
		$this->insert_revision_appointment( $params );
	}

	public function insert_revision_no_show_reverted_appointment( $appointment_id, $data_after, $meta_data ) {
		$params = array(
			'result'				 => 'success',
			'action'				 => 'no_show_reverted',
			'appointment_id' => $appointment_id,
			'data_after' 		 => $data_after,
			'data_before'		 => null
		);
		$this->insert_revision_appointment( $params );
	}

	public function insert_revision_appointment( $params ) {

		if ( empty( $params['action'] || empty( $this->get_action_title( $params['action'] ) ) ) ) {
			return;
		}

		// below: hints for WP.org to pick up phrases for translation
		// __( '{{ user }} changed the appointment status to {{ action }}', 'simply-schedule-appointments' );
		$revision = array(
			'result'         => $params['result'],
			'appointment_id' => $params['appointment_id'],
			'action'         => $params['action'],
			'action_title'	 => $this->get_action_title( $params['action'] ),
			'action_summary' => $this->get_action_summary( $params['action'] ),
			'summary_vars'   => array(
				'user'   		=> $this->get_user_name(),
				'staff' 		=> empty( $params['staff_ids'] ) ? array() : $params['staff_ids'],
				'action' 		=> $params['action'],
				'action_noun'   => empty( $params['action_noun'] ) ? null : $params['action_noun'],
				'action_verb'   => empty( $params['action_verb'] ) ? null : $params['action_verb'],
				'recipient_type'=> isset($params['recipient_type']) ? $params['recipient_type'] : null,
				'notification_type'=> isset($params['notification_type']) ? $params['notification_type'] : null,
				'notification_date'=> isset($params['notification_date']) ? $params['notification_date'] : null,
				'notification_time'=> isset($params['notification_time']) ? $params['notification_time'] : null,
				'business_previous_start_date' => isset( $params['business_previous_start_date'] ) ? $params['business_previous_start_date'] : null,
				'business_previous_start_time' => isset( $params['business_previous_start_time'] ) ? $params['business_previous_start_time'] : null,
				'business_start_date' => isset( $params['business_start_date'] ) ? $params['business_start_date'] : null,
				'business_start_time' => isset( $params['business_start_time'] ) ? $params['business_start_time'] : null,
				'notification_cancelation_reason' => isset( $params['notification_cancelation_reason'] ) ? $params['notification_cancelation_reason'] : null,
				'notification_title' => isset( $params['notification_title'] ) ? $params['notification_title'] : null,
			),
			'context'        => 'booking',
		);

		// in the revision_meta
		// only set meta_value_before if it has a value.
		// because if the field is set to null, the db will not insert the record.
		$revision_meta = array();

		// append appointment status changes
		if ( ! isset( $params['data_after']['status'] ) && isset( $params['data_before']['status'] ) ) {
			// If status is not set we assume it didn't change
			$params['data_after']['status'] = $params['data_before']['status'];
		}

		$revision_meta['status']['meta_value'] = $params['data_after']['status'];
		
		if ( isset( $params['data_before']['status'] ) ) {
			$revision_meta['status']['meta_value_before'] = $params['data_before']['status'];
		}

		// append appointment raw data changes
		$revision_meta['raw_data']['meta_value'] = $params['data_after'];
		if ( isset( $params['data_before'] ) ) {
			$revision_meta['raw_data']['meta_value_before'] = $params['data_before'];
		}

		// insert revision
		$this->insert_revision( $revision, $revision_meta );
	}

	// signarture of function that renders the action summary
	// $this->plugin->templates->render_template_string('{{ user }} changed the appointment status to {{ action }}',['user'=>'name','action'=>'action'])

	// =========================================================================
	//
	// main re-usable function - use to insert revisions and revisions meta data
	//
	// =========================================================================
	public function insert_revision( $revision = array(), $revision_meta = array() ) {
		if ( ! isset( $revision['result'] ) ) {
			ssa_debug_log( 'must specify result to create revision', 10 );
			ssa_debug_log( print_r( $revision, true ), 10 ); //phpcs:ignore
			return;
		}
		if (
			! isset( $revision['appointment_id'] ) &&
			! isset( $revision['appointment_type_id'] ) &&
			! isset( $revision['staff_id'] ) &&
			! isset( $revision['payment_id'] ) &&
			! isset( $revision['async_action_id'] ) ) {
				ssa_debug_log( 'must reference at least one foreign key to create revision', 10 );
				ssa_debug_log( print_r( $revision, true ), 10 ); //phpcs:ignore
				return;
		}
		if ( ! isset( $revision['action'] ) ) {
			ssa_debug_log( "action field must be set to create revision\n", 10 );
			ssa_debug_log( print_r( $revision, true ), 10 ); //phpcs:ignore
			return;
		}
		if ( ! isset( $revision['action_summary'] ) ) {
			ssa_debug_log( "action_summary field must be set to create revision\n", 10 );
			ssa_debug_log( print_r( $revision, true ), 10 ); //phpcs:ignore
			return;
		}
		if ( ! isset( $revision['context'] ) ) {
			ssa_debug_log( "context field must be set to create revision\n", 10 );
			ssa_debug_log( print_r( $revision, true ), 10 ); //phpcs:ignore
			return;
		}

		// Call & get the stack trace for each revision before insert
		$stack_trace = ssa_get_stack_trace();

		// merge with default values
		$revision = wp_parse_args(
			$revision,
			array(
				'user_id'      => get_current_user_id(),
				'summary_vars' => array(),
				'stack_trace'  => $this->parse_stack_trace_before_insert( $stack_trace )
			)
		);
		
		// pass on an array of meta data to be batch inserted under this revision's id in the meta table
		if ( ! empty( $revision_meta ) ) {
			$revision['meta_data'] = $revision_meta;
		}
		
		// insert revision and get its id
		$revision_id = $this->insert( $revision );

		return $revision_id;
	}

	public function prepare_item_for_response( $item, $recursive = 0 ) {
		$item = parent::prepare_item_for_response( $item, $recursive );

		if ( $recursive >= 0 ) {
			$item['action_title'] = __( $item['action_title'], 'simply-schedule-appointments' );
			$item['action_summary_populated'] = $this->popuplate_action_summary_for_response( $item );
		}

		return $item;
	}

	public function get_action_title( $action, $result = 'success' ) {
		// Only get/edit action titles here
		$action_titles = array(
			'synced_successfully' => 'Appointment Synced',
			'failed_to_sync' => 'Appointment Failed to Sync',
			'first_booked' => 'Appointment Booked',
			'booked' => 'Appointment Booked',
			'canceled' => 'Appointment Canceled',
			'rescheduled' => 'Appointment Rescheduled',
			'edited'	=> 'Appointment Edited',
			'abandoned' => 'Appointment Abandoned',
			'pending_payment' => 'Appointment\'s Payment Pending',
			'pending_form' => 'Appointment\'s Form Pending',
			'no_show' => 'Marked as No Show',
			'no_show_reverted' => 'No Show Reverted',
			'assigned' => 'Appointment Assigned',
			'reassigned' => 'Appointment Reassigned',
			'notification_scheduled' => 'Notification Scheduled',
			'notification_with_duration' => 'Notification Scheduled',
			'reminder' => 'Notification Scheduled',
			'notification_sent' => 'Notification Sent',
			'notification_canceled' => 'Notification Canceled',
			'notification_not_sent' => 'Notification Not Sent',
			'publish' => 'Appointment Type Created',
			'delete' => 'Appointment Type Deleted',
			'recover' => 'Appointment Type Recovered',
			'title_changed' => 'Title Changed',
			'min_booking_notice_changed' => 'Notice Required Changed',
			'buffer_before_changed' => 'Buffer Before Changed',
			'buffer_after_changed' => 'Buffer After Changed',
			'max_event_count_changed' => 'Per Day Limit Changed',
			'status_changed' => 'Appointment Type Recovered',
			'capacity_changed' => 'Capacity Changed',
			'capacity_type_changed' => 'Capacity Type Changed',
			'timezone_style_changed' => 'Timezone Changed',
			'duration_changed' => 'Duration Changed',
			'label_id_changed' => 'Label Color changed',
			'slug_changed' => 'Slug changed',
			'instructions_changed' => 'Instructions changed',
			'availability_increment_changed' => 'Availability Start Times Changed',
			'availability_type_changed' => 'Availability Type Changed',
			'availability_changed'=> 'Availability Schedule Changed',
			'web_meetings_changed'=> 'Web Meetings Changed',
			'booking_flow_settings_changed'=> 'Booking Flow Changed',
			'customer_information_changed'=> 'Customer Information Changed',
			'staff_changed' => 'Team Members Changed',
			'staff_ids_changed' => 'Team Members Staff Changed',
			'staff_capacity_changed' => 'Staff Capacity Changed',
			'has_max_capacity_changed' => 'Max Capacity Changed',
			'reminder_sent' => 'Notification Sent',
			'reminder_not_sent'=>'Notification Not Sent',
			'max_booking_notice_changed' => 'Max Booking Notice Changed',
			'shared_calendar_event_changed' => 'Shared Calendar Event Changed',
			'opt_out_notification' => 'Opt Out Notification',
		);

		// Update the array below whenever needed
		if ( in_array( $action, array( 'sync_appointment_to_calendar' ) ) ) {
			return $result === 'success' ? $action_titles['synced_successfully'] : $action_titles['failed_to_sync'];
		}

		if ( isset( $action_titles[ $action ] ) ) {
			return $action_titles[$action];
		}

		// We shouldn't really end up here
		ssa_debug_log( "Action `$action` does not exist in get_action_title\n", 5 );
		return;
	}

	public function create_item_permissions_check( $request ) {
			// only ssa code should interact with this class
			return false;
	}

	/**
	 * Check if a given request has access to update a specific item
	 *
	 * @param WP_REST_Request $request Full data about the request.
	 * @return WP_Error|bool
	 */
	public function update_item_permissions_check( $request ) {
			// only ssa code should interact with this class
			return false;
	}

	/**
	 * Check and return the logged in user name
	 * Otherwise return 'A logged out user'
	 *
	 * @param array $data
	 * @return string
	 */
	public function get_user_name() {

		$current_user = wp_get_current_user();

		// We don't have a logged in user
		if ( empty( $current_user->ID ) ) {
			return __( 'A logged out user', 'simply-schedule-appointments');
		}

		// First check if who's booking/editing is a staff member
		if( class_exists( 'SSA_Staff_Model' ) ) {
			$staff_id = $this->plugin->staff_model->get_staff_id_for_user_id( $current_user->ID );

			if( ! empty( $staff_id ) ) {
				$staff = new SSA_Staff_Object( $staff_id );
				return $staff->display_name;
			}
		}

		if( ! empty( $current_user->display_name ) ) {
			return $current_user->display_name;
		}

		if( ! empty( $current_user->user_login ) ) {
			return $current_user->user_login;
		}

		// Just in case if all have failed
		return __( 'A logged out user', 'simply-schedule-appointments');

	}

	public function popuplate_action_summary_for_response( $item ) {
		/* translators: If found, between double curly braces {{ Should not be translated }}. Actions: booked, canceled, pending_payment.. */
		$action_summary = esc_html__( $item['action_summary'], 'simply-schedule-appointments' );
		$summary_vars = $item['summary_vars'];

		// Regular expression to match placeholders wrapped inside double curly braces
		$pattern = '/{{\s*(.*?)\s*}}/';

		// Check if the action_summary contains placeholders
		if ( preg_match( $pattern, $action_summary ) ) {
				// Replace the placeholders with actual values
				$action_summary = preg_replace_callback($pattern, function($matches) use ($summary_vars) {
						$placeholder = $matches[1];
						// Check if the placeholder corresponds to a valid variable name
						if ( isset( $summary_vars[ $placeholder ] ) ) {
								if ( $placeholder === 'action' ) {
									// Only allow translations for actions
									return __( $summary_vars[ $placeholder ], 'simply-schedule-appointments' );
								}
								if ( $placeholder === 'staff' ) {
									$staff_ids = $summary_vars[ $placeholder ];
									$staff_names = array();
									
									foreach ( $staff_ids as $staff_id ) {
										$name = SSA_Staff_Model::get_staff_name_by_id( $staff_id );
										array_push($staff_names, $name);
									}
									if ( empty( $staff_names ) ) {
										return __( 'no one', 'simply-schedule-appointments' );
									}
									return implode(", ", $staff_names);
								}
								return $summary_vars[ $placeholder ];
						} else {
								// Placeholder does not correspond to a valid variable name
								return $matches[0]; // return the original placeholder
						}
				}, $action_summary );
		}
		return $action_summary;

	}

	public function get_action_summary( $action = '' ) {
		switch ( $action ) {
			case 'first_booked':
				return 'Appointment booked by {{ user }}';
			case 'assigned':
				return 'Appointment assigned to {{ staff }}';
			case 'reassigned':
				return 'Appointment reassigned to {{ staff }}';
			case 'edited':
				return 'Appointment edited by {{ user }}';
			case 'rescheduled':
				return 'Appointment rescheduled by {{ user }} from {{ business_previous_start_date }} at {{ business_previous_start_time }} to {{ business_start_date }} at {{ business_start_time }}';
			case 'booked':
			case 'canceled':
			case 'abandoned':
			case 'pending_payment':
			case 'pending_form':
				return '{{ user }} changed the appointment status to {{ action }}';
			case 'no_show':
				return 'Appointment marked as no show by {{ user }}';
			case 'no_show_reverted':
				return 'No show Status reverted by {{ user }}';
			case 'notification_scheduled':
				return 'The notification was scheduled to inform the {{ recipient_type }} that an {{ action_noun }} has been {{ action_verb }}.';
			case 'notification_with_duration':
				return 'The notification was scheduled to be sent on {{ notification_date }} at {{ notification_time }} to inform the {{ recipient_type }} that an {{ action_noun }} has been {{ action_verb }}.';
			case 'reminder':
				return 'The notification was scheduled to be sent on {{ notification_date }} at {{ notification_time }} to remind the {{ recipient_type }} about the appointment.';
			case 'notification_sent':
				return 'The notification was sent by {{ notification_type }} to inform the {{ recipient_type }} that an {{ action_noun }} has been {{ action_verb }}.';
			case 'notification_not_sent':
				return "The notification by {{ notification_type }},  to inform the {{ recipient_type }} that an {{ action_noun }} has been {{ action_verb }}, could not be sent";
			case 'publish':
				return 'Appointment Type created by {{ user }}';
			case 'delete':
				return 'Appointment Type deleted by {{ user }}';
			case 'recover':
				return 'Appointment Type recovered by {{ user }}';
			case 'title_changed':
				return '{{ user }} changed the appointment type title from {{ old_field }} to {{ new_field }}';
			case 'min_booking_notice_changed':
				return '{{ user }} changed the notice required from {{ old_field }} to {{ new_field }}';
			case 'max_booking_notice_changed':
				return '{{ user }} changed how far in advance customers can book an appointment from {{ old_field }} to {{ new_field }}';
			case 'shared_calendar_event_changed':
				return '{{ user }} changed the shared event settings for google calendar';
			case 'buffer_before_changed':
				return '{{ user }} changed the buffer before from {{ old_field }} to {{ new_field }}';
			case 'buffer_after_changed':
				return '{{ user }} changed the buffer after from {{ old_field }} to {{ new_field }}';
			case 'max_event_count_changed':
				return '{{ user }} changed the maximum number of appointments from {{ old_field }} to {{ new_field }}';
			case 'status_changed':
				return '{{ user }} recovered this appointment type';
			case 'capacity_changed':
				return '{{ user }} changed the capacity from {{ old_field }} to {{ new_field }}';
			case 'capacity_type_changed':
				return '{{ user }} changed the capacity type from {{ old_field }} to {{ new_field }}';
			case 'timezone_style_changed':
				return '{{ user }} changed the timezone from {{ old_field }} to {{ new_field }}';
			case 'duration_changed':
				return '{{ user }} changed the duration form {{ old_field }} to {{ new_field }}';
			case 'label_id_changed':
				return '{{ user }} changed the label color';
			case 'slug_changed':
				return '{{ user }} changed the slug from {{ old_field }} to {{ new_field }}';
			case 'instructions_changed':
				return '{{ user }} changed the instructions to {{ new_field }}';
			case 'availability_type_changed':
				return '{{ user }} changed the availability type from {{ old_field }} to {{ new_field }}';
			case 'availability_changed':
					return '{{ user }} changed the availability schedule';
			case 'availability_increment_changed':
				return '{{ user }} changed the appointment start times from {{ old_field }} to {{ new_field }}';
			case 'web_meetings_changed':
				return '{{ user }} changed the web meetings settings';
			case 'booking_flow_settings_changed':
				return '{{ user }} changed the booking flow settings';
			case 'customer_information_changed':
				return '{{ user }} changed the customer information fields';	
			case 'staff_changed':
				return '{{ user }} changed the team member booking rules';
			case 'staff_ids_changed':
				return '{{ user }} changed the team members staff';
			case 'staff_capacity_changed':
				return '{{ user }} changed the team members staff capacity';
			case 'has_max_capacity_changed':
				return '{{ user }} switched the maximum capacity option ';
			case 'reminder_sent':
				return 'The notification was sent by {{ notification_type }} to remind the {{ recipient_type }} about the appointment';
			case 'reminder_not_sent':
				return 'The notification by {{ notification_type }} to remind the {{ recipient_type }} about the appointment could not be sent';
			case 'opt_out_notification':
				return 'The user has not opted to receive notifications';
			case 'notification_canceled':
				return 'The "{{ notification_title }}" ({{ notification_type }}) notification to the {{ recipient_type }} was canceled for the following reason: {{ notification_cancelation_reason }}';
			default:
				return '{{ user }} changed the appointment status to {{ action }}';
		}
	}
  
	public function parse_stack_trace_before_insert( $string = '' ) {
		$pattern = '/#(\d+)\s+(.*)/';
		preg_match_all( $pattern, $string, $matches, PREG_SET_ORDER );

		$output = '';

		foreach ( $matches as $match ) {
			$order = $match[1];
			
			// Skip first two traces since these would always be for [0] -> ssa_get_stack_trace() and [1]-> SSA_Revision_Model->insert_revision
			if ( $order <= 1 ) {
				continue;
			}
			
			// Skip traces over 18 level deep
			if ( $order > 18 ) {
				break;
			}

			$output .= $match[0]."\n";
		}

		return $output;
	}

	public function insert_revision_on_notification_sent( $appointment_id, $response, $action_noun, $action_verb, $recipient_type,$notification_type,$data_after, $data_before) {
		if ($response === true || (is_array($response) && !in_array(false, $response))){
			$res = 'success';
			if($action_noun == 'appointment_start'){
				$action = 'reminder_sent';
			}
			else{
			$action = 'notification_sent';}
		}
		else{
			$res = 'failure';
			if($action_noun == 'appointment_start'){
				$action = 'reminder_not_sent';
			}
			else{
			$action = 'notification_not_sent';
		}
	}
		// Insert revision for notification sent
		$params = array(
			'result'            => $res,
			'action'            => $action,
			'appointment_id'    => $appointment_id,
			'data_after'        => $data_after,
			'data_before'       => $data_before,
			'action_noun'       => $action_noun,
			'action_verb'       => $action_verb,
			'recipient_type'    => $recipient_type,
			'notification_type' => $notification_type
		);

		$this->insert_revision_appointment( $params );
		}

	
	public function insert_revision_on_notification_canceled( $appointment_id, $data) {
		$defaults = [
			'data_after'                      => [],
			'data_before'                     => [],
			'recipient_type'                  => '',
			'notification_type'               => '',
			'notification_title'              => '',
			'notification_cancelation_reason' => '',
		];
	
		$data = array_merge($defaults, $data);

		$params = array(
			'result'            => 'success',
			'action'            => 'notification_canceled',
			'appointment_id'    => $appointment_id,
			'data_after'        => $data['data_after'],
			'data_before'       => $data['data_before'],
			'recipient_type'    => $data['recipient_type'],
			'notification_type' => $data['notification_type'],
			'notification_title'=> $data['notification_title'],
			'notification_cancelation_reason'=> $data['notification_cancelation_reason'],
		);

		$this->insert_revision_appointment( $params );
	}

	public function insert_revision_on_notification_scheduled( $appointment_id, $action_noun, $action_verb, $notification_date, $notification_time, $duration, $recipient_type, $data_after, $data_before) {
		if($duration > 0){
			if($action_noun == 'appointment_start'){
				$action = 'reminder';
			}else{
				$action = 'notification_with_duration';
			}
		}else{
			if($action_noun == 'appointment_start'){
				return ;
			}else{
			$action = 'notification_scheduled';
			}
		}
		// Insert revision for notification scheduled
		$params = array(
			'result'            => 'success',
			'action'            => $action,
			'appointment_id'    => $appointment_id,
			'data_after'        => $data_after,
			'data_before'       => $data_before,
			'action_noun'       => $action_noun,
			'action_verb'       => $action_verb,
			'recipient_type'    => $recipient_type,
			'notification_date'       => $notification_date,
			'notification_time'       => $notification_time,
		);

		$this->insert_revision_appointment( $params );
		$username = $this->get_user_name();
		}

		public function insert_revision_appointment_type( $params ) {

			if( empty( $params['action'] ) || empty( $this->get_action_title( $params['action'] ) ) ) {
				return; // This is not a supported action
			}

			$revision = array(
				'result'         => $params['result'],
				'appointment_type_id' => $params['appointment_type_id'],
				'action'         => $params['action'],
				'action_title'	 => $this->get_action_title( $params['action'] ),
				'action_summary' => $this->get_action_summary( $params['action'] ),
				'summary_vars'   => array(
					'user'   		=> $this->get_user_name(),
					'old_field'     => $params['old_field'],
					'new_field'     => $params['new_field']
				),
				'context'        => 'appointment type',
			);
	
			// in the revision_meta
			// only set meta_value_before if it has a value.
			// because if the field is set to null, the db will not insert the record.
			$revision_meta = array();
	
			// append appointment status changes
			if ( ! isset( $params['data_after']['status'] ) && isset( $params['data_before']['status'] ) ) {
				// If status is not set we assume it didn't change
				$params['data_after']['status'] = $params['data_before']['status'];
			}
	
			$revision_meta['status']['meta_value'] = $params['data_after']['status'];
			
			if ( isset( $params['data_before']['status'] ) ) {
				$revision_meta['status']['meta_value_before'] = $params['data_before']['status'];
			}
	
			// append appointment raw data changes
			$revision_meta['raw_data']['meta_value'] = $params['data_after'];
			if ( isset( $params['data_before'] ) ) {
				$revision_meta['raw_data']['meta_value_before'] = $params['data_before'];
			}
	
			// insert revision
			$this->insert_revision( $revision, $revision_meta );
		}

	public function insert_revision_created_appointment_type( $appointment_type_id, $data_after, $data_before = null) {

		if( gettype($data_after) !== 'array' ){
			$data_after = array();
		}

		if( gettype($data_before) !== 'array' ){
			$data_before = array();
		}

		$data_after['status'] = 'publish';
		$params = array(
			'result'			  => 'success',
			'action'			  => 'publish',
			'appointment_type_id' => $appointment_type_id,
			'old_field'           => '',
			'new_field'           => 'published',
			'data_after'		  => $data_after,
			'data_before'		  => $data_before
		);
		$this->insert_revision_appointment_type( $params );
	}
	
	public function insert_revision_deleted_appointment_type( $appointment_type_id, $data_after, $data_before) {

		if( gettype($data_after) !== 'array' ){
			$data_after = array();
		}

		if( gettype($data_before) !== 'array' ){
			$data_before = array();
		}

		$data_after['status'] = 'delete';
		$params = array(
			'result'		      => 'success',
			'action'			  => 'delete',
			'appointment_type_id' => $appointment_type_id,
			'old_field'           => 'published',
			'new_field'           => 'deleted',
			'data_after'		  => $data_after,
			'data_before'		  => $data_before
		);
		
		$this->insert_revision_appointment_type( $params );
	}

	public function insert_revision_updated_appointment_type( $appointment_type_id, $data_after, $data_before ) {
		if(empty($data_after) || empty($data_before)){
			return;
		}
		$changed_fields = $this->get_appt_type_changed_fields($data_after,$data_before);
		foreach($changed_fields as $changed_field){

			if ( empty( $data_after[ $changed_field ] ) || empty( $data_before[ $changed_field ] ) ) {
				continue;
			}

			$old_field = $data_before[$changed_field];
			$new_field = $data_after[$changed_field];
			if($changed_field !== 'capacity' && $changed_field !== 'max_event_count'){
				if (is_numeric($old_field) && is_int($old_field + 0)) {
					$old_field = $this->display_duration($old_field);
				}
				if (is_numeric($new_field) && is_int($new_field + 0)) {
					$new_field = $this->display_duration($new_field);
				}
			}
			
			$params = array(
				'result'          => 'success',
				'action'          => $changed_field.'_changed',
				'appointment_type_id' => $appointment_type_id,
				'old_field'       => $old_field ,
				'new_field'       => $new_field,
				'data_after'      => $data_after,
				'data_before'     => $data_before
			);
			$this->insert_revision_appointment_type( $params );
		}
	}

	public function get_appt_type_changed_fields($data_after,$data_before){		
		$changed_fields =[];

		foreach( $data_after as $key => $field ) {
			if ( empty( $data_before[ $key ] ) ) {
				continue;
			}

			// No Need to record the re-ordering of appointment types
			if ( $key === 'display_order'){
				continue;
			}

			if( $data_before [ $key ] !== $field ) {
				if ( $key === 'custom_customer_information') {
					$key = 'customer_information';
				}
				$changed_fields[] = $key;
			}
		}
		
		return $changed_fields;
	}

	//convert minutes to hours / days / weeks
	public function display_duration($duration) {
		$minutes = intval($duration);

		if ($minutes === 0) {
			return '0';
		}

		$type = 'minute';
		$num = $minutes;

		if ($minutes % (7 * 24 * 60) === 0) {
			$num = $minutes / (7 * 24 * 60);
			$type = 'week';
		} elseif ($minutes % (24 * 60) === 0) {
			$num = $minutes / (24 * 60);
			$type = 'day';
		} elseif ($minutes % 60 === 0) {
			$num = $minutes / 60;
			$type = 'hour';
		}
	
		$displayType = $num === 1 ? $type : $type . 's';
		return $num . ' ' . $displayType;
	}
	
}


