<?php
/**
 * Shared operational workflows for table reservations.
 *
 * @package FoodOrd
 */

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

/**
 * Class Foodord_Table_Reservation_Operations
 */
class Foodord_Table_Reservation_Operations {

	/**
	 * Reservations service.
	 *
	 * @var Foodord_Table_Reservations
	 */
	private $reservations;

	/**
	 * Reservation email service.
	 *
	 * @var Foodord_Table_Reservation_Emails
	 */
	private $emails;

	/**
	 * Constructor.
	 *
	 * @param Foodord_Table_Reservations|null       $reservations Reservations service.
	 * @param Foodord_Table_Reservation_Emails|null $emails       Reservation email service.
	 */
	public function __construct( $reservations = null, $emails = null ) {
		$this->reservations = $reservations instanceof Foodord_Table_Reservations
			? $reservations
			: new Foodord_Table_Reservations();
		$this->emails       = $emails instanceof Foodord_Table_Reservation_Emails
			? $emails
			: new Foodord_Table_Reservation_Emails();
	}

	/**
	 * Update a reservation status and dispatch matching notifications.
	 *
	 * @param int    $reservation_id Reservation ID.
	 * @param string $status         New reservation status.
	 * @param string $reason         Optional cancellation reason.
	 * @return object|WP_Error
	 */
	public function update_status( int $reservation_id, string $status, string $reason = '' ) {
		$result = $this->update_status_with_notification_result( $reservation_id, $status, $reason );
		if ( is_wp_error( $result ) ) {
			return $result;
		}

		return $result['reservation'];
	}

	/**
	 * Update a reservation status and return the notification result.
	 *
	 * @param int    $reservation_id Reservation ID.
	 * @param string $status         New reservation status.
	 * @param string $reason         Optional cancellation reason.
	 * @return array{reservation:object,notification_requested:bool,notification_sent:bool}|WP_Error
	 */
	public function update_status_with_notification_result( int $reservation_id, string $status, string $reason = '' ) {
		$previous = $this->reservations->get_reservation( $reservation_id );
		if ( ! $previous ) {
			return new WP_Error( 'foodord_reservation_invalid_id', __( 'Invalid reservation.', 'foodord' ), array( 'status' => 404 ) );
		}

		if ( $this->is_awaiting_payment( $previous ) ) {
			return new WP_Error(
				'foodord_reservation_payment_locked',
				__( 'This reservation is locked while the customer is completing payment.', 'foodord' ),
				array( 'status' => 409 )
			);
		}

		$result = $this->reservations->update_reservation_status( $reservation_id, $status, $reason );
		if ( is_wp_error( $result ) ) {
			return $result;
		}

		$current = $this->reservations->get_reservation( $reservation_id );
		if ( ! $current ) {
			return new WP_Error( 'foodord_reservation_invalid_id', __( 'Invalid reservation.', 'foodord' ), array( 'status' => 404 ) );
		}

		$notification = $this->maybe_send_status_email( $previous, $current );

		return array(
			'reservation'            => $current,
			'notification_requested' => $notification['requested'],
			'notification_sent'      => $notification['sent'],
		);
	}

	/**
	 * Update a confirmed reservation duration.
	 *
	 * @param int $reservation_id Reservation ID.
	 * @param int $duration       New duration in minutes.
	 * @return object|WP_Error
	 */
	public function update_duration( int $reservation_id, int $duration ) {
		return $this->reservations->update_confirmed_reservation_duration( $reservation_id, $duration );
	}

	/**
	 * Update a reservation guest count and notify the customer.
	 *
	 * @param int $reservation_id Reservation ID.
	 * @param int $party_size     New guest count.
	 * @return array{reservation:object,notification_requested:bool,notification_sent:bool}|WP_Error
	 */
	public function update_party_size_with_notification_result( int $reservation_id, int $party_size ) {
		$previous = $this->reservations->get_reservation( $reservation_id );
		if ( ! $previous ) {
			return new WP_Error( 'foodord_reservation_invalid_id', __( 'Invalid reservation.', 'foodord' ), array( 'status' => 404 ) );
		}

		if ( $this->is_awaiting_payment( $previous ) ) {
			return new WP_Error(
				'foodord_reservation_payment_locked',
				__( 'This reservation is locked while the customer is completing payment.', 'foodord' ),
				array( 'status' => 409 )
			);
		}

		if ( $this->has_ended( $previous ) ) {
			return new WP_Error( 'foodord_reservation_management_expired', __( 'This reservation can no longer be changed.', 'foodord' ), array( 'status' => 409 ) );
		}

		$previous_party_size = absint( $previous->party_size ?? 0 );
		$current             = $this->reservations->update_reservation_party_size( $reservation_id, $party_size );
		if ( is_wp_error( $current ) ) {
			return $current;
		}

		$notification_requested = absint( $current->party_size ?? 0 ) !== $previous_party_size
			&& $this->can_send_customer_status_email( $current );
		$notification_sent      = $notification_requested
			? $this->emails->send_updated_email( $current )
			: false;

		return array(
			'reservation'            => $current,
			'notification_requested' => $notification_requested,
			'notification_sent'      => $notification_sent,
		);
	}

	/**
	 * Resend the customer email matching the current reservation status.
	 *
	 * @param int $reservation_id Reservation ID.
	 * @return true|WP_Error
	 */
	public function resend_status_email( int $reservation_id ) {
		$reservation = $this->reservations->get_reservation( $reservation_id );
		if ( ! $reservation ) {
			return new WP_Error( 'foodord_reservation_invalid_id', __( 'Invalid reservation.', 'foodord' ), array( 'status' => 404 ) );
		}

		if ( $this->is_awaiting_payment( $reservation ) ) {
			return new WP_Error(
				'foodord_reservation_payment_locked',
				__( 'This reservation is locked while the customer is completing payment.', 'foodord' ),
				array( 'status' => 409 )
			);
		}

		if ( $this->has_ended( $reservation ) ) {
			return new WP_Error(
				'foodord_reservation_email_expired',
				__( 'Reservation email cannot be sent after the reservation has ended.', 'foodord' ),
				array( 'status' => 409 )
			);
		}

		if ( ! $this->send_customer_status_email( $reservation ) ) {
			return new WP_Error(
				'foodord_reservation_email_failed',
				__( 'Unable to send the reservation email.', 'foodord' ),
				array( 'status' => 500 )
			);
		}

		return true;
	}

	/**
	 * Get available time options for a staff-created reservation.
	 *
	 * @param string $date       Reservation date.
	 * @param int    $party_size Guest count, or zero before selection.
	 * @param bool   $allow_over_capacity Whether staff guest limits should be bypassed.
	 * @param int    $duration_minutes Optional staff-selected duration in minutes.
	 * @param bool   $ignore_booking_limits Whether maximum advance and late booking limits should be bypassed.
	 * @return array{times:string[],slots:array<int,array{date:string,time:string,default_duration_minutes:int}>,default_durations:array<string,int>}
	 */
	public function get_manual_time_options( string $date, int $party_size, bool $allow_over_capacity = false, int $duration_minutes = 0, bool $ignore_booking_limits = false ): array {
		$ignore_capacity_limits = $allow_over_capacity || Foodord_Table_Reservations::can_current_user_override_capacity_limits();
		$slots                  = $this->reservations->get_operational_time_options_for_date(
			Foodord_Table_Reservations::normalize_date_input( $date ),
			absint( $party_size ),
			0,
			$ignore_capacity_limits,
			$allow_over_capacity,
			Foodord_Table_Reservations::sanitize_optional_reservation_duration_minutes( $duration_minutes ),
			$ignore_booking_limits
		);
		$times                  = array();
		$default_durations      = array();
		foreach ( $slots as $slot ) {
			$time    = (string) $slot['time'];
			$times[] = $time;
			if ( $slot['default_duration_minutes'] > 0 ) {
				$default_durations[ $time ] = $slot['default_duration_minutes'];
			}
		}

		return array(
			'times'             => $times,
			'slots'             => $slots,
			'default_durations' => $default_durations,
		);
	}

	/**
	 * Get selectable tables and safe duration options for one staff slot.
	 *
	 * @param string $date                  Reservation date.
	 * @param string $time                  Reservation time.
	 * @param int    $party_size            Guest count.
	 * @param bool   $allow_over_capacity   Whether table capacity may be exceeded.
	 * @param bool   $ignore_booking_limits Whether maximum advance and late booking limits should be bypassed.
	 * @return array<int,array<string,mixed>>|WP_Error
	 */
	public function get_manual_table_options( string $date, string $time, int $party_size, bool $allow_over_capacity = false, bool $ignore_booking_limits = false ) {
		return $this->reservations->get_staff_table_options_for_slot(
			Foodord_Table_Reservations::normalize_date_input( $date ),
			sanitize_text_field( $time ),
			max( 1, absint( $party_size ) ),
			$allow_over_capacity,
			$ignore_booking_limits
		);
	}

	/**
	 * Create a short staff lease for one exact table and time.
	 *
	 * @param array<string,mixed> $data Hold data.
	 * @param bool                $allow_over_capacity Whether staff guest limits should be bypassed.
	 * @param bool                $ignore_booking_limits Whether maximum advance and late booking limits should be bypassed.
	 * @return array<string,mixed>|WP_Error
	 */
	public function create_staff_hold( array $data, bool $allow_over_capacity = false, bool $ignore_booking_limits = false ) {
		return $this->reservations->create_staff_hold_for_table(
			Foodord_Table_Reservations::normalize_date_input( (string) ( $data['date'] ?? '' ) ),
			sanitize_text_field( (string) ( $data['time'] ?? '' ) ),
			max( 1, absint( $data['party_size'] ?? 0 ) ),
			absint( $data['table_id'] ?? 0 ),
			$allow_over_capacity || Foodord_Table_Reservations::can_current_user_override_capacity_limits(),
			$ignore_booking_limits
		);
	}

	/**
	 * Refresh a staff lease heartbeat.
	 *
	 * @param string $hold_token Hold token.
	 * @return object|WP_Error
	 */
	public function refresh_staff_hold( string $hold_token ) {
		return $this->reservations->refresh_staff_hold( $hold_token );
	}

	/**
	 * Release a staff lease.
	 *
	 * @param string $hold_token Hold token.
	 * @return bool
	 */
	public function release_staff_hold( string $hold_token ): bool {
		return $this->reservations->release_staff_hold( $hold_token );
	}

	/**
	 * Create a reservation for a phone or in-person request.
	 *
	 * @param array<string,mixed> $data Reservation data.
	 * @param bool                $allow_over_capacity Whether staff guest limits should be bypassed.
	 * @param bool                $ignore_booking_limits Whether maximum advance and late booking limits should be bypassed.
	 * @return array{reservation:object,notification_requested:bool,notification_sent:bool}|WP_Error
	 */
	public function create_manual_reservation( array $data, bool $allow_over_capacity = false, bool $ignore_booking_limits = false ) {
		$date               = Foodord_Table_Reservations::normalize_date_input( (string) ( $data['date'] ?? '' ) );
		$time               = sanitize_text_field( (string) ( $data['time'] ?? '' ) );
		$party_size         = max( 1, absint( $data['party_size'] ?? 0 ) );
		$requested_duration = absint( $data['duration_minutes'] ?? 0 );
		$duration           = Foodord_Table_Reservations::sanitize_optional_reservation_duration_minutes( $requested_duration );
		$table_id           = absint( $data['table_id'] ?? 0 );
		$status             = Foodord_Table_Reservations::sanitize_status( (string) ( $data['status'] ?? '' ) );
		if ( ! in_array( $status, array( 'pending', 'confirmed' ), true ) ) {
			$status = 'confirmed';
		}

		$ignore_capacity_limits = $allow_over_capacity || Foodord_Table_Reservations::can_current_user_override_capacity_limits();
		$hold_token             = sanitize_text_field( (string) ( $data['hold_token'] ?? '' ) );
		if ( '' !== $hold_token ) {
			$hold = $this->reservations->prepare_staff_hold_for_reservation(
				$hold_token,
				$date,
				$time,
				$party_size,
				$ignore_capacity_limits,
				$duration
			);
		} elseif ( $table_id > 0 ) {
			if ( ! in_array( $requested_duration, Foodord_Table_Reservations::get_reservation_duration_options(), true ) ) {
				return new WP_Error( 'foodord_reservation_invalid_duration', __( 'Please select a valid reservation duration.', 'foodord' ) );
			}

			$staff_hold = $this->reservations->create_staff_hold_for_table(
				$date,
				$time,
				$party_size,
				$table_id,
				$ignore_capacity_limits,
				$ignore_booking_limits,
				$allow_over_capacity
			);
			if ( is_wp_error( $staff_hold ) ) {
				return $staff_hold;
			}

			$hold_token = sanitize_text_field( (string) ( $staff_hold['hold_token'] ?? '' ) );
			$hold       = $this->reservations->prepare_staff_hold_for_reservation(
				$hold_token,
				$date,
				$time,
				$party_size,
				$ignore_capacity_limits,
				$duration
			);
			if ( is_wp_error( $hold ) ) {
				$this->reservations->release_staff_hold( $hold_token );
				return $hold;
			}
		} else {
			$customer_session = $allow_over_capacity
				? sprintf( 'staff-%d-%s', get_current_user_id(), wp_generate_uuid4() )
				: 'admin-' . get_current_user_id();
			$hold             = $this->reservations->create_hold(
				$date,
				$time,
				$party_size,
				$customer_session,
				$ignore_capacity_limits,
				$allow_over_capacity,
				$duration,
				false,
				$ignore_booking_limits
			);
		}

		if ( is_wp_error( $hold ) ) {
			return $hold;
		}
		if ( '' === $hold_token && is_array( $hold ) ) {
			$hold_token = (string) ( $hold['hold_token'] ?? '' );
		}

		$result = $this->reservations->create_reservation_from_hold(
			$hold_token,
			array(
				'name'                   => sanitize_text_field( (string) ( $data['name'] ?? '' ) ),
				'email'                  => sanitize_email( (string) ( $data['email'] ?? '' ) ),
				'phone'                  => sanitize_text_field( (string) ( $data['phone'] ?? '' ) ),
				'note'                   => sanitize_textarea_field( (string) ( $data['note'] ?? '' ) ),
				'apply_deposit'          => false,
				'ignore_capacity_limits' => $ignore_capacity_limits,
			)
		);
		if ( is_wp_error( $result ) ) {
			$this->reservations->release_hold( $hold_token );
			return $result;
		}

		if ( $status !== (string) $result['status'] ) {
			$status_update = $this->reservations->update_reservation_status( (int) $result['reservation_id'], $status );
			if ( is_wp_error( $status_update ) ) {
				return $status_update;
			}
		}

		$reservation = $this->reservations->get_reservation( (int) $result['reservation_id'] );
		if ( ! $reservation ) {
			return new WP_Error( 'foodord_reservation_invalid_id', __( 'Invalid reservation.', 'foodord' ) );
		}

		$notification_requested = ! empty( $data['send_notification'] );
		$notification_sent      = $notification_requested
			? $this->send_customer_status_email( $reservation )
			: false;

		return array(
			'reservation'            => $reservation,
			'notification_requested' => $notification_requested,
			'notification_sent'      => $notification_sent,
		);
	}

	/**
	 * Send an email when a reservation moves to a customer-facing status.
	 *
	 * @param object|null $previous Previous reservation row.
	 * @param object|null $current  Current reservation row.
	 * @return array{requested:bool,sent:bool}
	 */
	public function maybe_send_status_email( $previous, $current ): array {
		$result = array(
			'requested' => false,
			'sent'      => false,
		);

		if ( ! $previous || ! $current ) {
			return $result;
		}

		$previous_status = Foodord_Table_Reservations::sanitize_status( (string) $previous->status );
		$current_status  = Foodord_Table_Reservations::sanitize_status( (string) $current->status );
		if ( $previous_status === $current_status || $this->has_ended( $current ) ) {
			return $result;
		}

		if ( in_array( $current_status, array( 'pending', 'confirmed' ), true ) ) {
			$result['requested'] = true;
			$result['sent']      = $this->send_customer_status_email( $current );

			return $result;
		}

		if ( 'cancelled' === $current_status ) {
			$result['requested'] = true;
			$result['sent']      = $this->send_customer_status_email( $current );
			$this->emails->send_admin_cancelled_email( $current );
		}

		return $result;
	}

	/**
	 * Send the customer email matching the reservation's current status.
	 *
	 * @param object $reservation Reservation row.
	 */
	public function send_customer_status_email( $reservation ): bool {
		$status = Foodord_Table_Reservations::sanitize_status( (string) $reservation->status );
		if ( in_array( $status, array( 'pending', 'confirmed' ), true ) ) {
			$manage_token = $this->reservations->prepare_manage_token( (int) $reservation->id );
			if ( '' !== $manage_token ) {
				$reservation->manage_token = $manage_token;
			}
		}

		if ( 'pending' === $status ) {
			return $this->emails->send_pending_email( $reservation );
		}

		if ( 'confirmed' === $status ) {
			return $this->emails->send_confirmed_email( $reservation );
		}

		if ( 'cancelled' === $status ) {
			return $this->emails->send_cancelled_email( $reservation );
		}

		return false;
	}

	/**
	 * Check whether the current status email can be sent manually.
	 *
	 * @param object $reservation Reservation row.
	 */
	public function can_send_customer_status_email( $reservation ): bool {
		$status = Foodord_Table_Reservations::sanitize_status( (string) $reservation->status );
		$email  = isset( $reservation->customer_email ) ? sanitize_email( (string) $reservation->customer_email ) : '';

		return in_array( $status, array( 'pending', 'confirmed', 'cancelled' ), true )
			&& is_email( $email )
			&& ! $this->has_ended( $reservation );
	}

	/**
	 * Check whether the reservation is locked during customer payment.
	 *
	 * @param object $reservation Reservation row.
	 */
	public function is_awaiting_payment( $reservation ): bool {
		$status         = isset( $reservation->status ) ? Foodord_Table_Reservations::sanitize_status( (string) $reservation->status ) : '';
		$payment_status = isset( $reservation->payment_status ) ? sanitize_key( (string) $reservation->payment_status ) : '';

		return 'paid_pending' === $status && 'requires_payment' === $payment_status;
	}

	/**
	 * Check whether the reservation has already ended.
	 *
	 * @param object $reservation Reservation row.
	 */
	public function has_ended( $reservation ): bool {
		$date = isset( $reservation->reservation_date ) ? (string) $reservation->reservation_date : '';
		$time = isset( $reservation->reservation_time ) ? (string) $reservation->reservation_time : '';

		if ( '' === $date || '' === $time ) {
			return false;
		}

		if ( 5 === strlen( $time ) ) {
			$time .= ':00';
		}

		$starts_at = DateTimeImmutable::createFromFormat( 'Y-m-d H:i:s', $date . ' ' . $time, wp_timezone() );
		if ( ! $starts_at instanceof DateTimeImmutable ) {
			return false;
		}

		$duration = Foodord_Table_Reservations::sanitize_reservation_duration_minutes( $reservation->duration_minutes ?? 90 );
		$ends_at  = $starts_at->modify( '+' . $duration . ' minutes' );

		return $ends_at instanceof DateTimeImmutable && $ends_at <= new DateTimeImmutable( 'now', wp_timezone() );
	}
}
