<?php
/*
 * Copyright (c) 2022 LatePoint LLC. All rights reserved.
 */

class OsProcessJobsHelper {

	public static function create_jobs_for_process( OsProcessModel $process, array $objects ) {
		if ( ! $process->check_if_objects_satisfy_trigger_conditions( $objects ) ) {
			return;
		}
		$job             = new OsProcessJobModel();
		$job->process_id = $process->id;
		$job->object_id  = $objects[0]['model_ready']->id;

		// check if job exists already
		$existing_job = new OsProcessJobModel();
		$exists       = $existing_job->select( 'id' )->where(
			[
				'process_id' => $job->process_id,
				'object_id'  => $job->object_id,
				'status'     => LATEPOINT_JOB_STATUS_SCHEDULED,
			] 
		)->get_results();
		if ( $exists ) {
			return;
		}

		$job_settings = [];
		foreach ( $process->actions as $action ) {
			if ( $action->status != LATEPOINT_STATUS_ACTIVE ) {
				continue;
			}
			$action->selected_data_objects = $objects;
			$action->prepare_data_for_run();
			$job_settings['action_info'][ $action->id ] = [ 'type' => $action->type ];
			$job_settings['action_data'][ $action->id ] = $action->prepared_data_for_run;
		}

		$event_time_utc    = null;
		$object_model_type = null;
		switch ( $process->event_type ) {
			case 'booking_updated':
			case 'booking_created':
			case 'booking_start':
			case 'booking_end':
				$object_model_type = 'booking';
				break;
			case 'customer_created':
			case 'customer_updated':
				$object_model_type = 'customer';
				break;
			case 'order_created':
			case 'order_updated':
				$object_model_type = 'order';
				break;
			case 'service_created':
			case 'service_updated':
				$object_model_type = 'service';
				break;
			case 'agent_created':
			case 'agent_updated':
				$object_model_type = 'agent';
				break;
			case 'transaction_created':
			case 'transaction_updated':
				$object_model_type = 'transaction';
				break;
			case 'payment_request_created':
				$object_model_type = 'payment_request';
				break;
		}
		/**
		 * Determine a type of a model based on a process
		 *
		 * @since 5.1.0
		 * @hook latepoint_get_object_model_type_for_process
		 *
		 * @param {string} $object_model_type Type of model
		 * @param {OsProcessModel} $process Process object
		 * @param {array} $objects Array of objects used for a process
		 *
		 * @returns {string} Filtered type of model
		 */
		$object_model_type = apply_filters( 'latepoint_get_object_model_type_for_process', $object_model_type, $process, $objects );
		try {
			switch ( $process->event_type ) {
				case 'booking_updated':
				case 'order_updated':
					$event_time_utc = new OsWpDateTime( $objects[0]['model_ready']->updated_at, new DateTimeZone( 'UTC' ) );
					break;
				case 'order_created':
				case 'booking_created':
				case 'transaction_created':
				case 'customer_created':
				case 'payment_request_created':
					$event_time_utc = new OsWpDateTime( $objects[0]['model_ready']->created_at, new DateTimeZone( 'UTC' ) );
					break;
				case 'booking_start':
					$event_time_utc = new OsWpDateTime( $objects[0]['model_ready']->start_datetime_utc, new DateTimeZone( 'UTC' ) );
					break;
				case 'booking_end':
					$event_time_utc = new OsWpDateTime( $objects[0]['model_ready']->end_datetime_utc, new DateTimeZone( 'UTC' ) );
					break;
			}

			/**
			 * Determine UTC event time based on a process
			 *
			 * @since 5.1.0
			 * @hook latepoint_get_event_time_utc_for_process
			 *
			 * @param {string} $event_time_utc Event time in UTC
			 * @param {OsProcessModel} $process Process object
			 * @param {array} $objects Array of objects used for a process
			 *
			 * @returns {string} Filtered event time in UTC
			 */
			$event_time_utc = apply_filters( 'latepoint_get_event_time_utc_for_process', $event_time_utc, $process, $objects );
		} catch ( Exception $e ) {
			OsDebugHelper::log( 'Error creating jobs for workflow', 'process_jobs_error', print_r( $process->id, true ) . ' ' . print_r( $objects, true ) . ' ' . $e->getMessage() );
			return;
		}
		if ( empty( $event_time_utc ) ) {
			return;
		}

		$job->settings     = wp_json_encode( $job_settings );
		$job->process_info = wp_json_encode( $process->get_info() );

		// apply time offset if exists in process
		$modify_by = self::should_modify_event_time( $process );
		if ( ! empty( $modify_by ) ) {
			$event_time_utc->modify( $modify_by );
		}
		$now_utc = new OsWpDateTime( 'now', new DateTimeZone( 'UTC' ) );

		// we need to make sure we are not creating jobs that are already past their relevance (e.g. booking updated but
		// 2 day before booking_start notification was already sent before, so scheduling a new job for that doesn't
		// make sense). Problem is that is we have booking_udpated notification - then the time of "booking_updated" event
		// is technically in the past, since the DB update happened couple of milliseconds ago. So solution is to create a
		// buffer time to allow for these discrepancies in which we can resend the notification if it's within that buffer
		$buffer                  = '+5 minutes';
		$event_time_utc_buffered = clone $event_time_utc;
		$event_time_utc_buffered->modify( $buffer );

		// event time with buffer is already long passed (cron already ran it probably, or after changing booking times, this job is not relevant anymore)
		if ( $event_time_utc_buffered < $now_utc ) {
			return;
		}


		$is_in_the_future       = $event_time_utc > $now_utc;
		$job->object_model_type = $object_model_type ?? 'n/a';
		$job->to_run_after_utc  = $event_time_utc->format( LATEPOINT_DATETIME_DB_FORMAT );

		$job->status = LATEPOINT_JOB_STATUS_SCHEDULED;
		$job->save();
		// execute immediately only when there is no configured time offset AND the event is not in the future;
		// if an offset is set, always defer to the cron runner so the delay is honoured even on edge cases
		// where the computed time lands at or just before now (e.g. "1 minute after" on a slow server).
		$has_time_offset = ! empty( $process->time_offset );
		if ( ! $is_in_the_future && ! $has_time_offset ) {
			$job->run();
		}
	}

	/**
	 * @param string $event_type
	 * @param array $objects example format: ['model' => 'booking', 'id' => $booking->id, 'model_ready' => OsModel $booking]
	 * @return void
	 */
	public static function create_jobs_for_event( string $event_type, array $objects ) {
		$processes = new OsProcessModel();
		// find all processes that match this event type
		$processes = $processes->where( [ 'event_type' => $event_type ] )->should_be_active()->get_results_as_models();
		if ( $processes ) {
			foreach ( $processes as $process ) {
				$process->build_from_json();
				self::create_jobs_for_process( $process, $objects );
			}
		}
	}

	/**
	 *
	 * Searches existing records that match this process conditions and schedules a job, for example if you created a new
	 * process that sends a notification 15 minute before the booking start - this method will find those bookings and
	 * schedule jobs to send notification
	 *
	 * @param OsProcessModel $process
	 * @return bool|void
	 */
	public static function recreate_jobs_for_existing_records( OsProcessModel $process ) {
		// don't create jobs for booking_updated event, since we don't capture the exact information of what was updated in
		// the booking and can't check if event conditions are satisfied
		if ( ! in_array( $process->event_type, [ 'booking_start', 'booking_end', 'booking_created', 'customer_created', 'transaction_created' ] ) ) {
			return false;
		}

		// calculate the cutoff date to search records that could be affected by this event
		$cutoff_datetime_utc = OsTimeHelper::now_datetime_utc();
		$modify_by           = self::should_modify_event_time( $process, true );
		if ( $modify_by ) {
			$cutoff_datetime_utc->modify( $modify_by );
		} else {
			// if there is no time offset for this process and event type is not "start" or "end" of the booking - we don't need to
			// create any jobs, since other events have already past
			if ( ! in_array( $process->event_type, [ 'booking_start', 'booking_end' ] ) ) {
				return true;
			}
		}

		$formatted_cutoff_utc = $cutoff_datetime_utc->format( LATEPOINT_DATETIME_DB_FORMAT );

		$args = [];
		switch ( $process->event_type ) {
			case 'booking_start':
				$args['start_datetime_utc >='] = $formatted_cutoff_utc;
				break;
			case 'booking_end':
				$args['end_datetime_utc >='] = $formatted_cutoff_utc;
				break;
			case 'booking_created':
				$args['created_at >='] = $formatted_cutoff_utc;
				break;
			case 'booking_updated':
				$args['updated_at >='] = $formatted_cutoff_utc;
				break;
			case 'transaction_created':
				$args['created_at >='] = $formatted_cutoff_utc;
				break;
			case 'customer_created':
				$args['created_at >='] = $formatted_cutoff_utc;
				break;
		}
		if ( $process->event_type == 'customer_created' ) {
			$models     = new OsCustomerModel();
			$model_name = 'customer';
		} else {
			$models     = new OsBookingModel();
			$model_name = 'booking';
		}
		if ( $args ) {
			$models->where( $args );
		}
		$models = $models->get_results_as_models();

		foreach ( $models as $model ) {
			$objects   = [];
			$objects[] = [
				'model'       => $model_name,
				'id'          => $model->id,
				'model_ready' => $model,
			];
			self::create_jobs_for_process( $process, $objects );
		}
	}

	public static function process_scheduled_jobs() {
		$jobs = new OsProcessJobModel();
		// find jobs that are scheduled to run in a period from [24 hour ago to NOW] - so that we don't run old irrelevant jobs
		$jobs = $jobs->where(
			[
				'status'              => LATEPOINT_JOB_STATUS_SCHEDULED,
				'to_run_after_utc <=' => OsTimeHelper::now_datetime_utc_in_db_format(),
				'to_run_after_utc >=' => OsTimeHelper::custom_datetime_utc_in_db_format( '-24 hours' ),
			] 
		)->get_results_as_models();
		foreach ( $jobs as $job ) {
			$job->run();
			$result = json_decode( $job->run_result, true );
			echo '<div>' . esc_html( $job->id ) . ':' . esc_html( $result['status'] ) . ', ' . esc_html( $result['message'] ) . '</div>';
		}
	}


	public static function init_hooks() {
		add_action( 'latepoint_customer_created', 'OsProcessJobsHelper::handle_customer_created', 12 );
		add_action( 'latepoint_transaction_created', 'OsProcessJobsHelper::handle_transaction_created', 12 );
		add_action( 'latepoint_booking_created', 'OsProcessJobsHelper::handle_booking_created', 12 );
		add_action( 'latepoint_booking_updated', 'OsProcessJobsHelper::handle_booking_updated', 12, 2 );
		add_action( 'latepoint_order_created', 'OsProcessJobsHelper::handle_order_created', 12 );
		add_action( 'latepoint_order_updated', 'OsProcessJobsHelper::handle_order_updated', 12, 2 );
		add_action( 'latepoint_payment_request_created', 'OsProcessJobsHelper::handle_payment_request_created', 12 );
	}

	public static function handle_customer_created( OsCustomerModel $customer ) {
		$objects   = [];
		$objects[] = [
			'model'       => 'customer',
			'id'          => $customer->id,
			'model_ready' => $customer,
		];
		self::create_jobs_for_event( 'customer_created', $objects );
	}

	public static function handle_transaction_created( OsTransactionModel $transaction ) {
		$objects   = [];
		$objects[] = [
			'model'       => 'transaction',
			'id'          => $transaction->id,
			'model_ready' => $transaction,
		];
		self::create_jobs_for_event( 'transaction_created', $objects );
	}

	public static function get_nice_job_status_name( $status ) {
		$names = [
			LATEPOINT_JOB_STATUS_COMPLETED => __( 'Completed', 'latepoint' ),
			LATEPOINT_JOB_STATUS_SCHEDULED => __( 'Scheduled', 'latepoint' ),
			LATEPOINT_JOB_STATUS_CANCELLED => __( 'Cancelled', 'latepoint' ),
			LATEPOINT_JOB_STATUS_ERROR     => __( 'Error', 'latepoint' ),
		];
		return $names[ $status ] ?? __( 'n/a', 'latepoint' );
	}

	public static function handle_booking_created( OsBookingModel $booking ) {
		$objects   = [];
		$objects[] = [
			'model'       => 'booking',
			'id'          => $booking->id,
			'model_ready' => $booking,
		];
		self::create_jobs_for_event( 'booking_created', $objects );
		self::create_jobs_for_event( 'booking_start', $objects );
		self::create_jobs_for_event( 'booking_end', $objects );
	}


	public static function handle_booking_updated( OsBookingModel $new_booking, OsBookingModel $old_booking ) {
		// remove previously scheduled jobs for this booking because it's changed and might not need them anymore
		// remove only those that are in "scheduled" status, those that were already sent or errored should stay
		$jobs = new OsProcessJobModel();
		$jobs->delete_where(
			[
				'status'            => LATEPOINT_JOB_STATUS_SCHEDULED,
				'object_id'         => $new_booking->id,
				'object_model_type' => 'booking',
			] 
		);

		$objects   = [];
		$objects[] = [
			'model'       => 'booking',
			'id'          => $new_booking->id,
			'model_ready' => $new_booking,
		];
		$objects[] = [
			'model'       => 'old_booking',
			'id'          => $old_booking->id,
			'model_ready' => $old_booking,
		];
		self::create_jobs_for_event( 'booking_updated', $objects );
		// some changes might have triggered other webhooks (e.g. service changed, so now it could be required to be reminded of booking start/end)

		self::create_jobs_for_event( 'booking_start', $objects );
		self::create_jobs_for_event( 'booking_end', $objects );
	}



	public static function handle_order_created( OsOrderModel $order ) {
		$objects   = [];
		$objects[] = [
			'model'       => 'order',
			'id'          => $order->id,
			'model_ready' => $order,
		];
		self::create_jobs_for_event( 'order_created', $objects );
	}


	public static function handle_payment_request_created( OsPaymentRequestModel $payment_request ) {
		$objects   = [];
		$objects[] = [
			'model'       => 'payment_request',
			'id'          => $payment_request->id,
			'model_ready' => $payment_request,
		];
		self::create_jobs_for_event( 'payment_request_created', $objects );
	}


	public static function handle_order_updated( OsOrderModel $new_order, OsOrderModel $old_order ) {
		// remove previously scheduled jobs for this order because it's changed and might not need them anymore
		// remove only those that are in "scheduled" status, those that were already sent or errored should stay
		$jobs = new OsProcessJobModel();
		$jobs->delete_where(
			[
				'status'            => LATEPOINT_JOB_STATUS_SCHEDULED,
				'object_id'         => $new_order->id,
				'object_model_type' => 'order',
			] 
		);

		$objects   = [];
		$objects[] = [
			'model'       => 'order',
			'id'          => $new_order->id,
			'model_ready' => $new_order,
		];
		$objects[] = [
			'model'       => 'old_order',
			'id'          => $old_order->id,
			'model_ready' => $old_order,
		];
		self::create_jobs_for_event( 'order_updated', $objects );
	}

	/**
	 * @param OsProcessModel $process
	 * @param $opposite determines if apply time offset in opposite direction (to search events eligible for cutoff)
	 * @return string
	 */
	public static function should_modify_event_time( OsProcessModel $process, $opposite = false ): string {
		if ( empty( $process->time_offset ) || empty( $process->time_offset['value'] ) || empty( $process->time_offset['unit'] ) ) {
			// no time offset, or incomplete offset data
			return '';
		}
		$time_offset_settings = $process->time_offset;
		$before_after         = $time_offset_settings['before_after'] ?? 'after';
		// offset, calculate how much to modify by
		$sign      = ( $opposite ) ? ( ( $before_after == 'after' ) ? '-' : '+' ) : ( ( $before_after == 'after' ) ? '+' : '-' );
		$modify_by = $sign . absint( $time_offset_settings['value'] ) . ' ' . $time_offset_settings['unit'];
		return $modify_by;
	}
}
