<?php
/**
 * MeetingBookingService
 *
 * Orchestrates the meeting booking flow:
 * validates slot, creates appointment, delegates to BookingService for
 * customer resolution and payment, fires hooks for emails/integrations.
 *
 * @package RadiusTheme\RadiusBooking\Services
 * @since   1.2.0
 */

namespace RadiusTheme\RadiusBooking\Services;

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

use RadiusTheme\RadiusBooking\Models\Appointment;
use RadiusTheme\RadiusBooking\Models\CustomerBooking;
use RadiusTheme\RadiusBooking\Repositories\MeetingTypeRepository;
use RadiusTheme\RadiusBooking\Repositories\AppointmentRepository;

/**
 * Class MeetingBookingService
 */
class MeetingBookingService {

	/**
	 * @var MeetingTypeRepository
	 */
	private MeetingTypeRepository $meeting_repo;

	/**
	 * @var MeetingAvailabilityService
	 */
	private MeetingAvailabilityService $availability_service;

	/**
	 * @var BookingService
	 */
	private BookingService $booking_service;

	/**
	 * @var CustomerService
	 */
	private CustomerService $customer_service;

	/**
	 * @var AppointmentRepository
	 */
	private AppointmentRepository $appointment_repo;

	/**
	 * MeetingBookingService constructor.
	 *
	 * @param MeetingTypeRepository      $meeting_repo         Meeting type repository.
	 * @param MeetingAvailabilityService $availability_service Availability calculation service.
	 * @param BookingService             $booking_service      Existing booking pipeline.
	 * @param CustomerService            $customer_service     Customer resolution service.
	 * @param AppointmentRepository      $appointment_repo     Appointment repository.
	 */
	public function __construct(
		MeetingTypeRepository $meeting_repo,
		MeetingAvailabilityService $availability_service,
		BookingService $booking_service,
		CustomerService $customer_service,
		AppointmentRepository $appointment_repo
	) {
		$this->meeting_repo         = $meeting_repo;
		$this->availability_service = $availability_service;
		$this->booking_service      = $booking_service;
		$this->customer_service     = $customer_service;
		$this->appointment_repo     = $appointment_repo;
	}

	/**
	 * Book a meeting by slug.
	 *
	 * @param string $slug The meeting type slug.
	 * @param array  $data Booking data with keys:
	 *                     - date (string Y-m-d)
	 *                     - start_time (string H:i)
	 *                     - bookings (array — customer info passed to BookingService)
	 *                     - payments (array — payment gateway info)
	 *
	 * @return array Booking result with appointment_id.
	 *
	 * @throws \Exception If module inactive, slot unavailable, or booking fails.
	 */
	public function bookMeeting( string $slug, array $data ): array {
		if ( ! rtrb_is_module_active( 'meeting' ) ) {
			throw new \Exception( esc_html__( 'Meeting module is not active.', 'radius-booking' ) );
		}

		$meeting_type = $this->meeting_repo->findBySlug( $slug );

		if ( ! $meeting_type ) {
			throw new \Exception( esc_html__( 'Meeting type not found.', 'radius-booking' ) );
		}

		$date       = sanitize_text_field( $data['date'] ?? '' );
		$start_time = sanitize_text_field( $data['start_time'] ?? '' );

		if ( empty( $date ) || empty( $start_time ) ) {
			throw new \Exception( esc_html__( 'Date and start time are required.', 'radius-booking' ) );
		}

		// Verify the slot is still available.
		$available_slots = $this->availability_service->getAvailableSlots( $meeting_type->id, $date );
		$slot_found      = false;

		foreach ( $available_slots as $slot ) {
			if ( $slot['start_time'] === $start_time ) {
				$slot_found = true;
				break;
			}
		}

		if ( ! $slot_found ) {
			throw new \Exception( esc_html__( 'Selected time slot is no longer available.', 'radius-booking' ) );
		}

		// Compute end time.
		$start_timestamp = strtotime( $date . ' ' . $start_time );
		$end_timestamp   = $start_timestamp + ( (int) $meeting_type->duration * 60 );
		$end_time        = gmdate( 'H:i:s', $end_timestamp );
		$start_time_full = gmdate( 'H:i:s', $start_timestamp );

		// Determine appointment status.
		$status = $meeting_type->requires_confirmation ? 'pending' : 'confirmed';

		// Create the appointment record.
		$appointment = Appointment::create(
			array(
				'employee_id'      => $meeting_type->employee_id,
				'service_id'       => 0,
				'appointment_date' => $date,
				'start_time'       => $start_time_full,
				'end_time'         => $end_time,
				'status'           => $status,
				'booking_type'     => 'meeting',
				'meeting_type_id'  => $meeting_type->id,
				'notify_customer'  => true,
			)
		);

		if ( ! $appointment || ! $appointment->get_id() ) {
			throw new \Exception( esc_html__( 'Failed to create appointment.', 'radius-booking' ) );
		}

		$appointment_id = $appointment->get_id();

		// Prepare data for BookingService — restructure for the pipeline.
		$booking_data                     = array();
		$booking_data['appointment_date'] = $date;
		$booking_data['start_time']       = $start_time_full;
		$booking_data['end_time']         = $end_time;
		$booking_data['employee_id']      = $meeting_type->employee_id;
		$booking_data['service_id']       = 0;
		$booking_data['booking_type']     = 'meeting';
		$booking_data['notes']            = sanitize_text_field( $data['notes'] ?? '' );

		// Read custom-field answers from either top-level `custom_info` (the
		// shape sent by the public meeting form) or from `bookings[0].custom_info`
		// (the legacy shape, kept for forward-compat with future flat-clients
		// that wrap their booking like the service form does).
		$raw_custom_info = $data['custom_info']
			?? ( $data['bookings'][0]['custom_info'] ?? array() );
		$custom_info     = $this->sanitizeCustomInfo( $raw_custom_info );

		// Wrap customer info into the bookings array format expected by BookingService.
		$booking_data['bookings'] = array(
			array(
				'customer_info' => array(
					'first_name' => sanitize_text_field( $data['first_name'] ?? '' ),
					'last_name'  => sanitize_text_field( $data['last_name'] ?? '' ),
					'email'      => sanitize_email( $data['email'] ?? '' ),
					'phone'      => sanitize_text_field( $data['phone'] ?? '' ),
					'name'       => trim(
						sanitize_text_field( $data['first_name'] ?? '' ) . ' ' .
						sanitize_text_field( $data['last_name'] ?? '' )
					),
				),
				'duration'    => (int) $meeting_type->duration * 60,
				'custom_info' => $custom_info,
			),
		);

		// Set meeting price so payment records and gateway processing get the correct amount.
		$meeting_price               = (float) ( $meeting_type->price ?? 0 );
		$booking_data['grand_total'] = $meeting_price;
		$booking_data['total']       = $meeting_price;
		$booking_data['payments']    = $data['payments'] ?? array();

		// Map PayPal data to the format BookingService expects (under 'paypal' key with camelCase).
		$payments = $data['payments'] ?? array();
		if ( 'paypal' === ( $payments['getway'] ?? '' ) ) {
			$booking_data['paypal'] = array(
				'orderId'   => $payments['paypal_order_id'] ?? '',
				'payerId'   => $payments['paypal_payer_id'] ?? '',
				'captureId' => $payments['paypal_capture_id'] ?? '',
				'status'    => $payments['status'] ?? '',
				'amount'    => $payments['amount'] ?? $meeting_price,
			);
		}

		// Delegate to BookingService for customer resolution, booking record, payment.
		$this->booking_service->createBookingsForAppointment( $booking_data, $appointment_id );

		/**
		 * Fires after a meeting is booked — used by integrations (Google, Zoom, etc.).
		 * Runs BEFORE email hooks so that meeting links are available in emails.
		 *
		 * @since 1.2.0
		 *
		 * @param Appointment $appointment  The created appointment.
		 * @param object      $meeting_type The meeting type (with integrations/settings).
		 * @param array       $data         The original booking data.
		 */
		do_action( 'radius_booking_meeting_booked', $appointment, $meeting_type, $data );

		/**
		 * Fires after meeting bookings are created — for pro integrations (Zoom, etc.).
		 * Provides the same data structure as radius_booking_after_create_bookings
		 * but includes the MeetingType object for settings checks.
		 *
		 * @since 1.3.0
		 *
		 * @param array  $bookings       The bookings array with customer info.
		 * @param array  $booking_data   The prepared booking data.
		 * @param int    $appointment_id The appointment ID.
		 * @param object $meeting_type   The MeetingType model.
		 */
		do_action( 'radius_booking_after_create_meeting_bookings', $booking_data['bookings'], $booking_data, $appointment_id, $meeting_type );

		/**
		 * Fires the meeting status hook to trigger emails.
		 * Runs AFTER integration hooks so meeting links are available in email data.
		 *
		 * @since 1.2.0
		 */
		do_action( 'radius_booking_meeting_status_' . $status, $appointment, $meeting_type );

		$customer_booking = CustomerBooking::where( 'appointment_id', $appointment_id )->first();
		$token            = $customer_booking->token ?? '';

		return array(
			'appointment_id' => $appointment_id,
			'token'          => $token,
			'status'         => $status,
			'meeting_type'   => array(
				'name'     => $meeting_type->name,
				'duration' => $meeting_type->duration,
				'slug'     => $meeting_type->slug,
			),
			'date'           => $date,
			'start_time'     => $start_time,
			'end_time'       => gmdate( 'H:i', $end_timestamp ),
		);
	}

	/**
	 * Normalize and sanitize a custom-field answers payload.
	 *
	 * Accepts a list of {field_id, label, type, value} entries from the
	 * public meeting form, drops malformed rows, and sanitizes each piece
	 * so the array is safe to persist into the JSON column on
	 * customer_bookings.custom_info.
	 *
	 * @param mixed $raw Raw payload value (expected to be an array).
	 *
	 * @return array Sanitized list of answer entries.
	 */
	private function sanitizeCustomInfo( $raw ): array {
		if ( ! is_array( $raw ) ) {
			return array();
		}

		$out = array();
		foreach ( $raw as $row ) {
			if ( ! is_array( $row ) ) {
				continue;
			}

			$field_id = isset( $row['field_id'] ) ? (int) $row['field_id'] : 0;
			if ( $field_id <= 0 ) {
				continue;
			}

			$value = $row['value'] ?? '';
			if ( is_array( $value ) ) {
				$value = array_map( 'sanitize_text_field', array_map( 'strval', $value ) );
			} else {
				$value = sanitize_textarea_field( (string) $value );
			}

			$out[] = array(
				'field_id' => $field_id,
				'label'    => sanitize_text_field( (string) ( $row['label'] ?? '' ) ),
				'type'     => sanitize_key( (string) ( $row['type'] ?? 'text' ) ),
				'value'    => $value,
			);
		}

		return $out;
	}

	/**
	 * Get a booking and its meeting type by the customer-booking token.
	 *
	 * Used by the public reschedule flow. Returns null if the token does not
	 * match an existing booking. Does not leak customer PII beyond what the
	 * booker themselves already submitted.
	 *
	 * @param string $token Customer booking token.
	 *
	 * @return array|null
	 */
	public function getBookingByToken( string $token ): ?array {
		$token = sanitize_text_field( $token );
		if ( empty( $token ) ) {
			return null;
		}

		$customer_booking = CustomerBooking::where( 'token', $token )->first();
		if ( ! $customer_booking ) {
			return null;
		}

		$appointment = Appointment::find( (int) $customer_booking->appointment_id );
		if ( ! $appointment || 'meeting' !== $appointment->booking_type ) {
			return null;
		}

		$meeting_type = $this->meeting_repo->find( (int) $appointment->meeting_type_id );
		if ( ! $meeting_type ) {
			return null;
		}

		return array(
			'booking'      => array(
				'token'          => $customer_booking->token,
				'appointment_id' => (int) $customer_booking->appointment_id,
				'status'         => $appointment->status,
				'date'           => $appointment->appointment_date,
				'start_time'     => substr( (string) $appointment->start_time, 0, 5 ),
				'end_time'       => substr( (string) $appointment->end_time, 0, 5 ),
				'is_past'        => strtotime( $appointment->appointment_date . ' ' . $appointment->start_time ) <= current_time( 'timestamp' ),
			),
			'meeting_type' => $meeting_type,
			'appointment'  => $appointment,
		);
	}

	/**
	 * Reschedule a meeting booking to a new date/time.
	 *
	 * Validates the token, enforces a minimum-notice window of 2 hours
	 * before the current start, checks the new slot against live
	 * availability (excluding this appointment's own block), updates the
	 * appointment, and fires a hook for email/integrations.
	 *
	 * @param string $token     Customer booking token.
	 * @param array  $data      Reschedule data ['date' => Y-m-d, 'start_time' => H:i].
	 *
	 * @return array Result with the refreshed booking shape.
	 *
	 * @throws \Exception If validation fails.
	 */
	public function rescheduleBooking( string $token, array $data ): array {
		if ( ! rtrb_is_module_active( 'meeting' ) ) {
			throw new \Exception( esc_html__( 'Meeting module is not active.', 'radius-booking' ) );
		}

		$context = $this->getBookingByToken( $token );
		if ( ! $context ) {
			throw new \Exception( esc_html__( 'Booking not found.', 'radius-booking' ) );
		}

		$appointment  = $context['appointment'];
		$meeting_type = $context['meeting_type'];
		$booking      = $context['booking'];

		if ( 'cancelled' === $appointment->status ) {
			throw new \Exception( esc_html__( 'This booking has been cancelled and cannot be rescheduled.', 'radius-booking' ) );
		}

		$current_ts = strtotime( $appointment->appointment_date . ' ' . $appointment->start_time );
		$now_ts     = (int) current_time( 'timestamp' );
		if ( $current_ts <= $now_ts ) {
			throw new \Exception( esc_html__( 'Past bookings cannot be rescheduled.', 'radius-booking' ) );
		}
		if ( ( $current_ts - $now_ts ) < ( 2 * HOUR_IN_SECONDS ) ) {
			throw new \Exception( esc_html__( 'Reschedule requires at least 2 hours notice before the meeting.', 'radius-booking' ) );
		}

		$new_date       = sanitize_text_field( $data['date'] ?? '' );
		$new_start_time = sanitize_text_field( $data['start_time'] ?? '' );
		if ( empty( $new_date ) || empty( $new_start_time ) ) {
			throw new \Exception( esc_html__( 'Date and start time are required.', 'radius-booking' ) );
		}

		if ( $new_date === $appointment->appointment_date && substr( (string) $appointment->start_time, 0, 5 ) === $new_start_time ) {
			throw new \Exception( esc_html__( 'Please choose a different time.', 'radius-booking' ) );
		}

		$available_slots = $this->availability_service->getAvailableSlots( $meeting_type->id, $new_date, (int) $appointment->get_id() );
		$slot_found      = false;
		foreach ( $available_slots as $slot ) {
			if ( $slot['start_time'] === $new_start_time ) {
				$slot_found = true;
				break;
			}
		}
		if ( ! $slot_found ) {
			throw new \Exception( esc_html__( 'Selected time slot is no longer available.', 'radius-booking' ) );
		}

		$new_start_ts = strtotime( $new_date . ' ' . $new_start_time );
		$new_end_ts   = $new_start_ts + ( (int) $meeting_type->duration * 60 );

		$old_date = $appointment->appointment_date;
		$old_time = substr( (string) $appointment->start_time, 0, 5 );

		$appointment->update(
			array(
				'appointment_date' => $new_date,
				'start_time'       => gmdate( 'H:i:s', $new_start_ts ),
				'end_time'         => gmdate( 'H:i:s', $new_end_ts ),
			)
		);

		/**
		 * Fires after a meeting booking is rescheduled.
		 *
		 * @since 1.3.0
		 *
		 * @param Appointment $appointment  The updated appointment.
		 * @param object      $meeting_type The meeting type.
		 * @param string      $old_date     Previous date (Y-m-d).
		 * @param string      $old_time     Previous start time (H:i).
		 */
		do_action( 'radius_booking_meeting_rescheduled', $appointment, $meeting_type, $old_date, $old_time );

		return array(
			'token'          => $booking['token'],
			'appointment_id' => (int) $appointment->get_id(),
			'status'         => $appointment->status,
			'date'           => $new_date,
			'start_time'     => $new_start_time,
			'end_time'       => gmdate( 'H:i', $new_end_ts ),
			'meeting_type'   => array(
				'name'     => $meeting_type->name,
				'duration' => $meeting_type->duration,
				'slug'     => $meeting_type->slug,
			),
		);
	}
}
