<?php

/**
 * Booking Service
 */

namespace RadiusTheme\RadiusBooking\Services;

if ( ! defined( 'ABSPATH' ) ) {
	exit; // Exit if accessed directly
}

use DateTime;
use DateTimeZone;
use RadiusTheme\RadiusBooking\Core\Container\Container;
use RadiusTheme\RadiusBooking\Models\Appointment;
use RadiusTheme\RadiusBooking\Models\CustomerBooking;
use RadiusTheme\RadiusBooking\Models\Payment;
use RadiusTheme\RadiusBooking\Models\Service;
use RadiusTheme\RadiusBooking\Models\User;
use RadiusTheme\RadiusBooking\Helpers\SettingsHelper;
use RadiusTheme\RadiusBooking\Repositories\BookingRepository;
use RadiusTheme\RadiusBooking\Repositories\PaymentRepository;
/**
 * Services/BookingService.php
 * Business logic layer for Booking.
 */
class BookingService {

	/**
	 * Booking Repository
	 *
	 * @var BookingRepository
	 */
	private BookingRepository $bookingRepo;

	/**
	 * Customer Service
	 *
	 * @var CustomerService
	 */
	private CustomerService $customerService;

	/**
	 * Constructor for initializing the booking repository and customer service.
	 *
	 * @param BookingRepository $bookingRepo The booking repository instance.
	 * @param CustomerService $customerService The customer service instance.
	 *
	 * @return void
	 */
	public function __construct( BookingRepository $bookingRepo, CustomerService $customerService ) {
		$this->bookingRepo     = $bookingRepo;
		$this->customerService = $customerService;
	}

	/**
	 * Create bookings for a specific appointment.
	 *
	 * @param array $data An array of data containing booking details.
	 * @param int $appointment_id The ID of the appointment for which bookings are created.
	 * @param bool $is_recurring Indicates whether the appointment is recurring. Default is false.
	 *
	 * @return void
	 */
	public function createBookingsForAppointment( array $data, int $appointment_id, $is_recurring = false ) {
		// Filter data before processing
		$data = apply_filters( 'radius_booking_before_create_bookings', $data, $appointment_id );

		$bookings = $data['bookings'] ?? array();

		if ( ! is_array( $bookings ) || empty( $bookings ) ) {
			return;
		}

		// Action before creating bookings
		do_action( 'radius_booking_before_create_bookings_loop', $bookings, $data, $appointment_id );

		foreach ( $bookings as $booking ) {
			$this->createSingleBooking( $booking, $data, $appointment_id );
		}

		// Action after all bookings created
		do_action( 'radius_booking_after_create_bookings', $bookings, $data, $appointment_id );
	}

	/**
	 * Create a single booking with associated customer and payment details.
	 *
	 * @param array $booking Booking details used for creating the booking.
	 * @param array $appointmentData Appointment data including payment details and metadata.
	 * @param int $appointment_id Identifier for the specific appointment.
	 *
	 * @return mixed Returns the payment ID or the payment processing result upon success, or null on failure.
	 */
	public function createSingleBooking( array $booking, array $appointmentData, int $appointment_id ) {
		// Filter booking data before processing
		$booking = apply_filters( 'radius_booking_before_create_single_booking', $booking, $appointmentData, $appointment_id );

		// Action at the start of booking creation
		do_action( 'radius_booking_create_single_booking_start', $booking, $appointmentData, $appointment_id );

		$customer_id = $this->customerService->resolveCustomer( $booking );
		if ( ! $customer_id ) {
			// Action when customer resolution fails
			do_action( 'radius_booking_customer_resolution_failed', $booking, $appointmentData, $appointment_id );

			return;
		}

		// Action after customer resolved
		do_action( 'radius_booking_customer_resolved', $customer_id, $booking, $appointmentData, $appointment_id );

		// Get existing booking ID before create/update (needed to find existing payment if customer changes)
		$existingBooking = $this->bookingRepo->getBookingByAppointmentId( $appointment_id );
		$oldBookingId    = $existingBooking ? $existingBooking->id : 0;

		// Create or update booking
		$bookingData     = $this->prepareBookingData( $booking, $appointmentData, $appointment_id, $customer_id );
		$customerBooking = $this->createOrUpdateBooking( $appointment_id, $customer_id, $bookingData );

		if ( ! $customerBooking ) {
			// Action when booking creation fails
			do_action( 'radius_booking_creation_failed', $bookingData, $appointment_id, $customer_id );

			return null;
		}

		// Action after booking created
		do_action( 'radius_booking_created', $customerBooking, $bookingData, $appointment_id, $customer_id );

		$customerBookingId = $customerBooking->get_id();

		// Get payment gateway info
		$appointPaymentData = $appointmentData['payments'] ?? array();
		$paymentGateway     = $appointPaymentData['getway'] ?? 'offline';

		// Calculate the actual payment amount that will be charged
		$totalAmount = empty( $appointmentData['total'] )
			? $this->bookingRepo->calculateBookingPriceForAppointment( $appointmentData )
			: $appointmentData['total'];

		// Get the actual amount to be charged for THIS appointment
		$actualPaymentAmount = $this->calculatePaymentAmount(
			$totalAmount,
			$paymentGateway,
			$appointmentData
		);

		// Filter payment gateway
		$paymentGateway = apply_filters( 'radius_booking_payment_gateway', $paymentGateway, $appointPaymentData, $customerBookingId );

		// Filter the calculated payment amount
		$actualPaymentAmount = apply_filters( 'radius_booking_payment_amount', $actualPaymentAmount, $appointmentData, $customerBookingId );

		// Create or update payment record with the calculated amount
		$payment_id = $this->createOrUpdatePayment(
			$customerBookingId,
			$customer_id,
			$appointmentData,
			$oldBookingId
		);

		// Dispatch email notifications now that payment record is available.
		// Only fire the generic appointment status hook for service bookings.
		// Meetings and events fire their own status hooks in their respective services.
		$bookingType = $appointmentData['booking_type'] ?? 'service';
		if ( ! in_array( $bookingType, array( 'meeting', 'event' ), true ) ) {
			$appointmentStatus = $customerBooking->get_appointment_status();
			$appointmentId     = $customerBooking->get_appointment_id();
			do_action( 'radius_booking_appointment_status_' . $appointmentStatus, $appointmentId, $customerBooking );
		}

		// Only process payment if amount > 0
		if ( $actualPaymentAmount > 0 ) {
			// Action before payment processing
			do_action( 'radius_booking_before_process_payment', $payment_id, $customerBookingId, $paymentGateway, $actualPaymentAmount );

			$result = $this->processPaymentByGateway(
				$paymentGateway,
				$payment_id,
				$customerBookingId,
				$customer_id,
				$appointmentData,
				$appointPaymentData
			);

			// Action after payment processing
			do_action( 'radius_booking_after_process_payment', $result, $payment_id, $customerBookingId, $paymentGateway );

			return $result;
		}

		// For zero amount bookings (offline or recurring with no payment required)
		do_action( 'radius_booking_zero_amount_booking', $payment_id, $customerBookingId, $customer_id );

		return $payment_id;
	}

	/**
	 * Creates or updates a booking based on the provided appointment ID, customer ID,
	 * and booking data. Handles scenarios where an existing booking needs to be updated
	 * or deleted, or where a new booking needs to be created.
	 *
	 * @param int $appointment_id The ID of the appointment associated with the booking.
	 * @param int $customer_id The ID of the customer for whom the booking is being created or updated.
	 * @param array $bookingData An associative array of booking data used for creating or updating a booking.
	 *
	 * @return object The newly created or updated booking object.
	 */
	private function createOrUpdateBooking( int $appointment_id, int $customer_id, array $bookingData ) {
		// Filter booking data before create/update
		$bookingData = apply_filters( 'radius_booking_data_before_save', $bookingData, $appointment_id, $customer_id );

		$getBooking = $this->bookingRepo->getBookingByAppointmentId( $appointment_id );

		if ( $getBooking ) {
			if ( $getBooking->customer_id != $customer_id ) {
				// Different customer - delete old booking and create new
				do_action( 'radius_booking_before_delete_old_booking', $getBooking, $customer_id, $bookingData );

				$this->bookingRepo->delete( $getBooking->id );

				do_action( 'radius_booking_after_delete_old_booking', $getBooking->id, $customer_id );

				$newBooking = $this->bookingRepo->create( $bookingData );

				do_action( 'radius_booking_created_after_delete', $newBooking, $getBooking->id );

				return $newBooking;
			} else {
				// Same customer - update existing booking
				do_action( 'radius_booking_before_update', $getBooking, $bookingData );

				$this->bookingRepo->update( $getBooking->id, $bookingData );
				$updatedBooking = $this->bookingRepo->find( $getBooking->id );

				do_action( 'radius_booking_after_update', $updatedBooking, $getBooking );

				return $updatedBooking;
			}
		}

		// No existing booking - create new
		do_action( 'radius_booking_before_create_new', $bookingData, $appointment_id, $customer_id );

		$newBooking = $this->bookingRepo->create( $bookingData );

		do_action( 'radius_booking_after_create_new', $newBooking, $bookingData );

		return $newBooking;
	}

	/**
	 * Create or update a payment record associated with a booking.
	 *
	 * @param int $booking_id The ID of the booking associated with the payment.
	 * @param int $customer_id The ID of the customer making the payment.
	 * @param array $appointmentData Data related to the appointment, including payment and booking details.
	 * @param int $old_booking_id Optional. The ID of an existing booking to update payment for. Defaults to 0.
	 *
	 * @return int The ID of the created or updated payment record.
	 */
	private function createOrUpdatePayment(
		int $booking_id,
		int $customer_id,
		array $appointmentData,
		int $old_booking_id = 0
	) {
		$appointPaymentData = $appointmentData['payments'] ?? array();
		$paymentGateway     = $appointPaymentData['getway'] ?? 'offline';

//		$deposit_amount = $appointmentData['deposit_amount'] ?? 0;
//		$deposit_payment = $appointmentData['deposit_payment'] ?? false;
//		$deposit_payment_type = $appointmentData['deposit_payment_type'] ?? 'full';

		// Calculate the base payment amount
		$totalAmount = empty( $appointmentData['grand_total'] )
			? $this->bookingRepo->calculateBookingPriceForAppointment( $appointmentData )
			: $appointmentData['grand_total'];

		// Determine payment amount based on payment logic
		$paymentAmount = empty( $appointmentData['total'] ) ?  $this->calculatePaymentAmount(
			$totalAmount,
			$paymentGateway,
			$appointmentData
		) : $appointmentData['total'];




		if ( ! empty( $appointmentData['cart_items'] ) ) {
			$total_items = count( $appointmentData['cart_items'] ) + 1;

			// Calculate grand total: use explicit value if provided, otherwise derive from per-item service price.
			$grandTotal = ! empty( $appointmentData['grand_total'] )
				? (float) $appointmentData['grand_total']
				: $totalAmount * $total_items;

			$totalAmount = $grandTotal / $total_items;

			// Only split payment amount if it was explicitly provided and non-zero.
			if ( ! empty( $appointmentData['total'] ) && (float) $appointmentData['total'] > 0 ) {
				$paymentAmount = (float) $appointmentData['total'] / $total_items;
			}
		}

		if('offline' === $paymentGateway){
			$paymentAmount = 0;
		}

		// Calculate tax if enabled (Pro feature).
		$tax_settings = SettingsHelper::get_setting( 'tax' );
		$tax_rate     = 0;
		$tax_amount   = 0;

		if ( rtrb_pro_active() && ! empty( $tax_settings['enabled'] ) && ! empty( $tax_settings['rate'] ) ) {
			$tax_rate = (float) $tax_settings['rate'];

			// If the frontend already sent grand_total (tax-inclusive) and tax_amount,
			// use those directly to avoid double-taxation.
			$frontend_tax = $appointmentData['tax_amount'] ?? null;
			if ( $frontend_tax !== null && isset( $appointmentData['grand_total'] ) && $appointmentData['grand_total'] !== '' && $appointmentData['grand_total'] !== null ) {
				$tax_amount = round( (float) $frontend_tax, 2 );
				// $totalAmount already equals grand_total (tax-inclusive from frontend) — do NOT add tax again.
			} else {
				// Fallback: backend calculates tax (e.g., admin-created appointments).
				if ( ! empty( $tax_settings['inclusive'] ) ) {
					$tax_amount = $totalAmount - ( $totalAmount / ( 1 + $tax_rate / 100 ) );
				} else {
					$tax_amount   = $totalAmount * ( $tax_rate / 100 );
					$totalAmount += $tax_amount;
				}
				$tax_amount = round( $tax_amount, 2 );
			}
		}

		$dueAmount = $totalAmount - $paymentAmount;

		// Determine initial payment status
		// If amount is 0, status should be 'pending' (not 'paid')
		// Only actual paid amounts should have status changed to 'paid' after gateway processing
		$paymentStatus = 'pending';

		if($paymentAmount > 0 && $paymentAmount <= $totalAmount ){
			$paymentStatus = 'partial_payment';
		}

		$paymentData = array(
			'booking_id'        => $booking_id,
			'customer_id'       => $customer_id,
			'amount'            => $paymentAmount,
			'total_amount'      => $totalAmount,
			'due_amount'        => $dueAmount,
			'booking_date_time' => $appointmentData['appointment_date'] . ' ' . ( $appointmentData['start_time'] ?? '' ),
			'status'            => $paymentStatus,
			'payment_method'    => $paymentGateway,
			'transaction_id'    => '',
			'booking_type'      => $appointmentData['booking_type'] ?? 'service',
			'tax_rate'          => $tax_rate,
			'tax_amount'        => $tax_amount,
		);

		// Add coupon data if present
		if ( ! empty( $appointmentData['coupon_id'] ) ) {
			$paymentData['coupon_id']        = (int) $appointmentData['coupon_id'];
			$paymentData['coupon_code']      = sanitize_text_field( $appointmentData['coupon_code'] ?? '' );
			$paymentData['discount_amount']  = (float) ( $appointmentData['discount_amount'] ?? 0 );
		}

		if ( ! empty( $appointmentData['parent_payment_id'] ) ) {
			$paymentData['parent_id'] = $appointmentData['parent_payment_id'];
		}

		// Filter payment data before save
		$paymentData = apply_filters( 'radius_booking_payment_data_before_save', $paymentData, $booking_id, $customer_id, $appointmentData );

		$currentPayment = $old_booking_id ? $this->bookingRepo->getBookingPayment( $old_booking_id ) : null;

		if ( $currentPayment ) {
			// Update existing payment
			do_action( 'radius_booking_before_update_payment', $currentPayment, $paymentData );

			Payment::find( $currentPayment->id )->update( $paymentData );

			do_action( 'radius_booking_after_update_payment', $currentPayment->id, $paymentData );

			return $currentPayment->id;
		}

		// Create new payment
		do_action( 'radius_booking_before_create_payment', $paymentData, $booking_id, $customer_id );

		$payment = Payment::create( $paymentData );

		do_action( 'radius_booking_after_create_payment', $payment, $paymentData );

		return $payment->id;
	}

	/**
	 * Calculate payment amount.
	 *
	 * @param float $totalAmount Total order amount.
	 * @param string $paymentGateway Payment gateway (offline, stripe, paypal, etc.).
	 * @param array $appointmentData Appointment data containing service_id and other info.
	 *
	 * @return float Calculated payment amount.
	 */
	private function calculatePaymentAmount( $totalAmount, $paymentGateway, $appointmentData ) {
		// Offline payments always have pending status with 0 amount
		if ( $paymentGateway === 'offline' ) {
			return 0;
		}

		// Allow pro plugin to modify payment amount (e.g. for recurring appointments)
		return apply_filters( 'rtrb_calculate_payment_amount', $totalAmount, $appointmentData, $totalAmount );
	}

	/**
	 * Process payment based on gateway type
	 *
	 * @param string $gateway Payment gateway identifier
	 * @param int $payment_id Payment record ID
	 * @param int $booking_id Booking record ID
	 * @param int $customer_id Customer ID
	 * @param array $appointmentData Full appointment data
	 * @param array $appointPaymentData Payment-specific data
	 *
	 * @return array Result with success status and details
	 */
	private function processPaymentByGateway(
		string $gateway,
		int $payment_id,
		int $booking_id,
		int $customer_id,
		array $appointmentData,
		array $appointPaymentData
	) {
		// Action before gateway routing
		do_action( 'radius_booking_before_gateway_routing', $gateway, $payment_id, $booking_id, $appointPaymentData );

		$result = null;

		switch ( strtolower( $gateway ) ) {
			case 'stripe':
				$result = apply_filters(
					'radius_booking_process_stripe_payment',
					null,
					$payment_id,
					$booking_id,
					$customer_id,
					$appointmentData,
					$appointPaymentData
				);

				if ( null === $result ) {
					$result = array(
						'success'    => false,
						'message'    => 'Stripe payments require the Pro plugin.',
						'payment_id' => $payment_id,
						'booking_id' => $booking_id,
					);
				}
				break;

			case 'paypal':
				$result = $this->processPayPalPayment(
					$payment_id,
					$booking_id,
					$appointmentData
				);
				break;

			case 'offline':
			default:
				// Action for custom gateway processing
				$customResult = apply_filters( 'radius_booking_process_custom_gateway', null, $gateway, $payment_id, $booking_id, $customer_id, $appointmentData, $appointPaymentData );

				if ( $customResult !== null ) {
					$result = $customResult;
				} else {
					$result = $this->processOfflinePayment(
						$payment_id,
						$booking_id
					);
				}
				break;
		}

		// Filter payment result
		$result = apply_filters( 'radius_booking_payment_result', $result, $gateway, $payment_id, $booking_id );

		// Action after gateway processing
		do_action( 'radius_booking_after_gateway_processing', $result, $gateway, $payment_id, $booking_id );

		return $result;
	}

	/**
	 * Process PayPal payment
	 *
	 * @param int $payment_id The unique identifier for the payment to be processed.
	 * @param int $booking_id The unique identifier for the associated booking.
	 * @param array $appointmentData The appointment data containing PayPal-related information.
	 *
	 * @return array The result of the PayPal payment processing, including success status, message, and transaction details.
	 */
	private function processPayPalPayment(
		int $payment_id,
		int $booking_id,
		array $appointmentData
	) {
		// Action before PayPal processing
		do_action( 'radius_booking_before_paypal_payment', $payment_id, $booking_id, $appointmentData );

		// Get PayPal info from appointment data
		$paypal = $appointmentData['paypal'] ?? array();

		// Validate PayPal data
		if ( empty( $paypal['captureId'] ) || empty( $paypal['status'] ) ) {
			Payment::find( $payment_id )->update(
				array(
					'status' => 'failed',
					'notes'  => 'PayPal payment data incomplete',
				)
			);

			// Action when PayPal data incomplete
			do_action( 'radius_booking_paypal_data_incomplete', $payment_id, $booking_id, $paypal );

			return array(
				'success'    => false,
				'message'    => 'PayPal payment data incomplete',
				'payment_id' => $payment_id,
				'booking_id' => $booking_id,
			);
		}

		// Determine payment status
		$paymentStatus = strtoupper( $paypal['status'] ) === 'COMPLETED' ? (new PaymentRepository())->getPaymentStatusByAmount( $payment_id) : 'failed';

		// Filter PayPal payment status
		$paymentStatus = apply_filters( 'radius_booking_paypal_payment_status', $paymentStatus, $paypal, $payment_id );

		// Prepare update data
		$updateData = array(
			'transaction_id'    => $paypal['captureId'] ?? '',
			'status'            => $paymentStatus,
			'paypal_order_id'   => $paypal['orderId'] ?? null,
			'paypal_payer_id'   => $paypal['payerId'] ?? null,
			'paypal_capture_id' => $paypal['captureId'] ?? null,
		);

		// Filter PayPal update data
		$updateData = apply_filters( 'radius_booking_paypal_update_data', $updateData, $paypal, $payment_id, $booking_id );

		// Update payment record
		Payment::find( $payment_id )->update( $updateData );

		if ( $paymentStatus === 'paid' ) {
			// Action after successful PayPal payment
			do_action( 'radius_booking_paypal_payment_success', $payment_id, $booking_id, $paypal );

			return array(
				'success'     => true,
				'message'     => 'PayPal payment processed successfully',
				'payment_id'  => $payment_id,
				'booking_id'  => $booking_id,
				'transaction' => array(
					'order_id'   => $paypal['orderId'] ?? '',
					'payer_id'   => $paypal['payerId'] ?? '',
					'capture_id' => $paypal['captureId'] ?? '',
				),
			);
		}

		// Action after failed PayPal payment
		do_action( 'radius_booking_paypal_payment_failed', $payment_id, $booking_id, $paypal );

		return array(
			'success'    => false,
			'message'    => 'PayPal payment not completed',
			'payment_id' => $payment_id,
			'booking_id' => $booking_id,
			'status'     => $paypal['status'] ?? 'unknown',
		);
	}

	/**
	 * Processes an offline payment for a specific booking by updating the payment status
	 * and triggering actions before and after the processing.
	 *
	 * @param int $payment_id The ID of the payment to process.
	 * @param int $booking_id The ID of the booking associated with the payment.
	 *
	 * @return array An associative array containing the success state, message, payment ID, booking ID, and updated status.
	 */
	private function processOfflinePayment(
		int $payment_id,
		int $booking_id
	) {
		// Action before offline payment processing
		do_action( 'radius_booking_before_offline_payment', $payment_id, $booking_id );

		// Update payment status to pending (will be confirmed manually)
		Payment::find( $payment_id )->update(
			array(
				'status' => 'pending',
				'notes'  => 'Awaiting offline payment confirmation',
			)
		);

		// Action after offline payment processed
		do_action( 'radius_booking_after_offline_payment', $payment_id, $booking_id );

		return array(
			'success'    => true,
			'message'    => 'Booking created successfully - offline payment pending',
			'payment_id' => $payment_id,
			'booking_id' => $booking_id,
			'status'     => 'pending',
		);
	}

	/**
	 * Prepares booking data based on the provided booking details, appointment information, and customer ID.
	 *
	 * @param array $booking An associative array containing booking details such as 'id', 'customer_info', 'custom_info', and 'duration'.
	 * @param array $appointmentData An associative array with information about the appointment used to calculate the booking price.
	 * @param int $appointment_id The ID of the appointment associated with the booking.
	 * @param int $customer_id The ID of the customer making the booking.
	 *
	 * @return array An associative array of prepared booking data, including details such as booking ID, appointment ID, customer ID, status, price, token, customer and custom info, duration, and UTC offset.
	 */
	public function prepareBookingData( array $booking, array $appointmentData, int $appointment_id, int $customer_id ): array {
		$this_tz_str = date_default_timezone_get();
		$this_tz     = new DateTimeZone( $this_tz_str );
		$now         = new DateTime( 'now', $this_tz );
		$offset      = $this_tz->getOffset( $now );

		if ( empty( $booking['customer_info']['first_name'] ) && empty( $booking['customer_info']['last_name'] ) && empty( $booking['customer_info']['email'] ) ) {
			$getCustomer = User::find( $customer_id );
			$profile     = $getCustomer->profile()->get();

			$booking['customer_info']['first_name'] = $profile->first_name ?? '';
			$booking['customer_info']['last_name']  = $profile->last_name ?? '';
			$booking['customer_info']['email']      = $getCustomer->email ?? '';
		}

		// Snapshot of dynamic pricing as it was applied at booking time. Only
		// stored when a rule actually adjusted the price; null otherwise so
		// admin views render dynamic pricing only on bookings that had it.
		$dynamic_pricing_snapshot = null;
		if ( ! empty( $appointmentData['dynamic_pricing']['applied_rules'] ) ) {
			$dynamic_pricing_snapshot = $appointmentData['dynamic_pricing'];
		}

		$bookingData = array(
			'id'              => $booking['id'] ?? null,
			'appointment_id'  => $appointment_id,
			'customer_id'     => $customer_id,
			'status'          => 'confirmed',
			'price'           => $this->bookingRepo->calculateBookingPriceForAppointment( $appointmentData ),
			'token'           => wp_generate_password( 10, false ),
			'customer_info'   => $booking['customer_info'] ?? array(),
			'custom_info'     => $booking['custom_info'] ?? array(),
			'duration'        => $booking['duration'] ?? 0,
			'utc_offset'      => $offset,
			'persons'         => $appointmentData['persons'] ?? 1,
			'dynamic_pricing' => $dynamic_pricing_snapshot,
		);

		// Filter prepared booking data
		return apply_filters( 'radius_booking_prepare_booking_data', $bookingData, $booking, $appointmentData, $appointment_id, $customer_id );
	}

}
