<?php
/**
 * RadiusBooking API Route Registrar
 *
 * This class is responsible for registering API routes dynamically with the WordPress REST API.
 * It supports CRUD operations and allows for middleware to be applied globally or per route.
 *
 * @package RadiusTheme\RadiusBooking\Core\Api\Routes
 */

namespace RadiusTheme\RadiusBooking\Services;

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

use DateTime;
use DateTimeZone;
use Exception;
use RadiusTheme\RadiusBooking\Core\Container\Container;
use RadiusTheme\RadiusBooking\Core\Support\Collection;
use RadiusTheme\RadiusBooking\Core\Support\DateTimeParser;
use RadiusTheme\RadiusBooking\Helpers\BookingHelper;
use RadiusTheme\RadiusBooking\Helpers\SettingsHelper;
use RadiusTheme\RadiusBooking\Models\Appointment;
use RadiusTheme\RadiusBooking\Models\Payment;
use RadiusTheme\RadiusBooking\Models\Service;
use RadiusTheme\RadiusBooking\PaymentGateway\PaymentService;
use RadiusTheme\RadiusBooking\Repositories\AppointmentRepository;
use RadiusTheme\RadiusBooking\Repositories\PaymentRepository;
use RadiusTheme\RadiusBooking\Repositories\ServiceRepository;
use RadiusTheme\RadiusBooking\Repositories\UserRepository;
use RadiusTheme\RadiusBooking\Services\EmployeeServices\UserWeekdayDataService;
use DateInterval;
use WP_Error;

/**
 * Services/AppointmentService.php
 * Business logic layer for appointments
 */
class AppointmentService {

	/**
	 * The appointment repository instance.
	 *
	 * @var AppointmentRepository
	 */
	private AppointmentRepository $appointmentRepo;
	/**
	 * The user repository instance.
	 *
	 * @var UserRepository
	 */

	private UserRepository $userRepo;

	/**
	 * The service repository instance.
	 *
	 * @var ServiceRepository
	 */

	private ServiceRepository $serviceRepo;

	/**
	 * The booking service instance.
	 *
	 * @var BookingService
	 */
	private BookingService $bookingService;

	/**
	 * The payment repository instance.
	 *
	 * @var PaymentRepository
	 */
	private PaymentRepository $paymentRepo;

	/**
	 * Class constructor to initialize the necessary repositories and services for handling appointments, users, services, bookings, and payments.
	 *
	 * @param AppointmentRepository $appointmentRepo The repository responsible for managing appointment-related data.
	 * @param UserRepository $userRepo The repository responsible for managing user-related data.
	 * @param ServiceRepository $serviceRepo The repository responsible for managing service-related data.
	 * @param BookingService $bookingService The service responsible for managing booking processes and logic.
	 * @param PaymentRepository $paymentRepo The repository responsible for managing payment-related data.
	 *
	 * @return void
	 */
	public function __construct(
		AppointmentRepository $appointmentRepo,
		UserRepository $userRepo,
		ServiceRepository $serviceRepo,
		BookingService $bookingService,
		PaymentRepository $paymentRepo
	) {
		$this->appointmentRepo = $appointmentRepo;
		$this->userRepo        = $userRepo;
		$this->serviceRepo     = $serviceRepo;
		$this->bookingService  = $bookingService;
		$this->paymentRepo     = $paymentRepo;
	}

	/**
	 * Reschedule an existing appointment (Pro feature).
	 *
	 * @param int    $id      Appointment ID.
	 * @param string $newDate New appointment date (Y-m-d).
	 * @param string $newTime New start time (H:i:s).
	 *
	 * @return array Result with success status and message.
	 */
	public function rescheduleAppointment( int $id, string $newDate, string $newTime ) {
		$result = apply_filters( 'radius_booking_reschedule_appointment', null, $id, $newDate, $newTime );

		if ( null !== $result ) {
			return $result;
		}

		return array(
			'success' => false,
			'message' => __( 'Appointment rescheduling requires the Pro plugin.', 'radius-booking' ),
		);
	}

	/**
	 * Confirm an appointment.
	 *
	 * @param int $id Appointment ID.
	 *
	 * @return bool True if status updated successfully.
	 */
	public function confirmAppointment( int $id ): bool {
		return $this->appointmentRepo->updateStatus( $id, 'confirmed' );
	}

	/**
	 * Mark an appointment as completed.
	 *
	 * @param int $id Appointment ID.
	 *
	 * @return bool True if status updated successfully.
	 */
	public function completeAppointment( int $id ): bool {
		return $this->appointmentRepo->updateStatus( $id, 'completed' );
	}

	/**
	 * Cancel an appointment with an optional reason.
	 *
	 * @param int $id Appointment ID.
	 * @param string|null $reason Cancellation reason.
	 *
	 * @return bool True if cancelled successfully.
	 */
	public function cancelAppointment( int $id, string $reason = null ): bool {
		return $this->appointmentRepo->cancelAppointment( $id, $reason );
	}

	/**
	 * Cancel an appointment with an optional reason.
	 *
	 * @param int $id Appointment ID.
	 *
	 * @return bool True if cancelled successfully.
	 */
	public function pendingAppointment( int $id ): bool {
		return $this->appointmentRepo->updateStatus( $id, 'pending' );
	}

	/**
	 * Get upcoming appointments.
	 *
	 * @param int $limit Maximum number of appointments to retrieve.
	 *
	 * @return array List of upcoming appointments.
	 */
	public function getUpcomingAppointments( int $limit = 10 ): array {
		return $this->appointmentRepo->findUpcoming( $limit );
	}

	/**
	 * Formats a given list of appointments into a structured array with details including customer name, service name,
	 * formatted date, and appointment status. A limit can be applied to restrict the number of appointments returned.
	 *
	 * @param array $appointments The list of appointments to format. Each appointment should include details such as
	 *                             customer information, service name, appointment date, and start time.
	 * @param int $limit Optional. The maximum number of appointments to include in the result. Defaults to 10.
	 *
	 * @return array A formatted array of appointments, where each entry includes the appointment ID, customer name,
	 *               service name, formatted date, and status.
	 */
	public function getUpcomingAppointmentsWithFormat( $appointments, int $limit = 10 ): array {
		$collection = new Collection( $appointments );

		return $collection->map(
			function ( $appointment ) {
				$customerName = $appointment['customer']->customer_info['name'] ?? 'Unknown';
				$payment      = $appointment['payment']->total_amount ?? '-';
				$employeeName = $appointment['employee_name'] ?? '';
				$serviceName  = $appointment['service_name'] ?? 'Service';

				// Format date & time
				$date          = DateTimeParser::parse( $appointment['appointment_date'] . ' ' . $appointment['start_time'] );
				$formattedDate = $date->isToday()
					? 'Today, ' . $date->format( 'g:i A' )
					: ( $date->isTomorrow()
						? 'Tomorrow, ' . $date->format( 'g:i A' )
						: $date->format( 'M d, g:i A' ) );

				return array(
					'id'            => $appointment['id'],
					'customer'      => $customerName,
					'employee_name' => $employeeName,
					'service'       => $serviceName,
					'date'          => $formattedDate,
					'payment'       => $payment,
					'status'        => $appointment['status'],
				);
			}
		)->toArray();
	}

	/**
	 * Get all appointments for a user.
	 *
	 * @param int $userId User ID.
	 *
	 * @return array List of user's appointments.
	 */
	public function getUserAppointments( int $userId ): array {
		return $this->appointmentRepo->findByUser( $userId );
	}

	/**
	 * Get appointments within a date range.
	 *
	 * @param string $startDate Start date (Y-m-d).
	 * @param string $endDate End date (Y-m-d).
	 *
	 * @return array List of appointments in date range.
	 */
	public function getAppointmentsByDateRange( string $startDate, string $endDate ): array {
		return $this->appointmentRepo->findByDateRange( $startDate, $endDate );
	}

	/**
	 * Get dashboard statistics about appointments.
	 *
	 * @return array Statistics with keys 'total', 'pending', 'confirmed', 'upcoming'.
	 */
	public function getDashboardStats(): array {
		return array(
			'total'     => $this->appointmentRepo->count(),
			'pending'   => count( $this->appointmentRepo->getPendingAppointments() ),
			'confirmed' => count( $this->appointmentRepo->getConfirmedAppointments() ),
			'upcoming'  => count( $this->appointmentRepo->findUpcoming() ),
		);
	}


	/**
	 * Processes appointment bookings by creating individual and recurring bookings for a given appointment.
	 *
	 * @param array $data The data required to create bookings, which may include customer details, schedules, and preferences.
	 * @param int $appointment_id The unique identifier of the appointment for which bookings are being processed.
	 *
	 * @return void This method does not return a value.
	 */
	public function processAppointmentBookings( array $data, int $appointment_id ): void {
		$this->bookingService->createBookingsForAppointment( $data, $appointment_id );
		do_action( 'rtrb_after_process_appointment_booking', $data, $appointment_id, $this );
	}

	/**
	 * Retrieves the available dates and scheduling information for a specific employee and service.
	 *
	 * @param int|string $employee_id The ID of the employee whose availability is being retrieved.
	 * @param int|string $service_id The ID of the service related to the employee's availability.
	 *
	 * @return array An associative array containing the employee's ID, their schedule (including available dates and events),
	 *               and metadata (total working days, special days, and days off).
	 */
	public function getEmployeeAvailableDateTime( $employee_id, $service_id ): array {
		$assigned_services = $this->userRepo->getUserServices( $employee_id );
		$matched_services  = array_filter(
			$assigned_services,
			function ( $srv ) use ( $service_id ) {
				return $srv['service_id'] == $service_id;
			}
		);

		$service_data           = array_values( $matched_services )[0] ?? null;
		$booking_advance_period = ! empty( $service_data['booking_advance_period'] ) ? $service_data['booking_advance_period'] : '365';
		$months_ahead           = (int) ceil( $booking_advance_period / 30 );

		$working_hours = UserWeekdayDataService::getEffectiveWeekdays( $employee_id );
		$special_days  = UserWeekdayDataService::getEffectiveSpecialDays( $employee_id );
		$days_off      = UserWeekdayDataService::getEffectiveDaysOff( $employee_id );

		// Prepare data for frontend
		return array(
			'employee_id' => $employee_id,
			'schedule'    => array(
				'available_dates' => $this->generateAvailableDates( $working_hours, $special_days, $days_off, $months_ahead ),
				'events'          => $this->generateCalendarEvents( $special_days, $days_off ),
			),
			'metadata'    => array(
				'total_working_days' => count(
					array_filter(
						$working_hours,
						function ( $day ) {
							return ! empty( $day['time_periods'] );
						}
					)
				),
				'total_special_days' => count( $special_days ),
				'total_days_off'     => count( $days_off ),
			),
		);
	}

	/**
	 * Generates a list of available dates for scheduling based on working hours, special days, and days off.
	 *
	 * @param array $working_hours An array defining the standard working hours for each day of the week.
	 * @param array $special_days An array containing special days with custom time periods or other overrides.
	 * @param array $days_off An array listing specific dates when no availability is allowed.
	 * @param int $months_ahead The number of months ahead to consider for generating available dates. Defaults to 36 months.
	 *
	 * @return array An array of available dates, where each entry includes the date, day name, and availability details.
	 */
	private function generateAvailableDates( $working_hours, $special_days, $days_off, $months_ahead = 36 ): array {
		$available_dates = array();
		$start_date      = new DateTime();
		$end_date        = clone $start_date;
		$end_date->add( new DateInterval( 'P' . $months_ahead . 'M' ) );

		// Create maps for quick lookup
		$days_off_map      = $this->createDaysOffMap( $days_off );
		$working_hours_map = $this->createWorkingHoursMap( $working_hours );
		$special_days_map  = $this->createSpecialDaysMap( $special_days );

		$current_date = clone $start_date;
		while ( $current_date <= $end_date ) {
			$date_str  = $current_date->format( 'Y-m-d' );
			$day_index = (int) $current_date->format( 'N' ); // 1 = Monday, 7 = Sunday

			// Skip if it's a day off
			if ( isset( $days_off_map[ $date_str ] ) ) {
				$current_date->add( new DateInterval( 'P1D' ) );
				continue;
			}

			// Check for special days first (they override regular schedule)
			if ( isset( $special_days_map[ $date_str ] ) ) {
				$special_day = $special_days_map[ $date_str ];
				if ( ! empty( $special_day['time_periods'] ) ) {
					$available_dates[] = array(
						'date'         => $date_str,
						'day_name'     => $current_date->format( 'l' ),
						'is_available' => true,
						'special_day'  => $special_day['name'],
					);
				}
			} elseif ( isset( $working_hours_map[ $day_index ] ) && ! empty( $working_hours_map[ $day_index ]['time_periods'] ) ) {
				$available_dates[] = array(
					'date'         => $date_str,
					'day_name'     => $current_date->format( 'l' ),
					'is_available' => true,
				);
			}

			$current_date->add( new DateInterval( 'P1D' ) );
		}

		return $available_dates;
	}

	/**
	 * Generates a map of days off, where the key is the date in 'Y-m-d' format,
	 * and the value is the corresponding day off details.
	 *
	 * @param array $days_off An array of days off, each containing 'start_date' and 'end_date' keys.
	 *
	 * @return array An associative array mapping dates to their respective day off details.
	 */
	private function createDaysOffMap( $days_off ): array {
		$map = array();
		foreach ( $days_off as $day_off ) {
			$current = new DateTime( $day_off['start_date'] );
			$end     = new DateTime( $day_off['end_date'] );

			while ( $current <= $end ) {
				$map[ $current->format( 'Y-m-d' ) ] = $day_off;
				$current->add( new DateInterval( 'P1D' ) );
			}
		}

		return $map;
	}

	/**
	 * Creates a map of working hours based on the provided working hours data.
	 *
	 * @param array $working_hours An array of working hours data, where each element typically includes a 'day_index' and associated scheduling information.
	 *
	 * @return array An associative array where the keys are the day indices and the values are the corresponding working hours data for each day.
	 */
	private function createWorkingHoursMap( $working_hours ): array {
		$map = array();
		foreach ( $working_hours as $weekday ) {
			$map[ $weekday['day_index'] ] = $weekday;
		}

		return $map;
	}

	/**
	 * Creates a mapping of dates to their corresponding special day details.
	 *
	 * @param array $special_days An array of special day data, where each item includes a 'start_date' and 'end_date'.
	 *
	 * @return array An associative array where each key is a date (formatted as 'Y-m-d') and the value is the corresponding special day information.
	 */
	private function createSpecialDaysMap( $special_days ): array {
		$map = array();
		foreach ( $special_days as $special_day ) {
			$current = new DateTime( $special_day['start_date'] );
			$end     = new DateTime( $special_day['end_date'] );

			while ( $current <= $end ) {
				$map[ $current->format( 'Y-m-d' ) ] = $special_day;
				$current->add( new DateInterval( 'P1D' ) );
			}
		}

		return $map;
	}


	/**
	 * Formats a given time string into a 24-hour format (HH:mm).
	 *
	 * @param string $time The time string to be formatted.
	 *
	 * @return string The formatted time in HH:mm format.
	 */
	private function formatTime( $time ): string {
		return gmdate( 'H:i', strtotime( $time ) );
	}

	/**
	 * Calculates the duration in minutes between two time points.
	 *
	 * @param string $start_time The starting time in a recognizable date-time format (e.g., "HH:MM").
	 * @param string $end_time The ending time in a recognizable date-time format (e.g., "HH:MM").
	 *
	 * @return int The duration in minutes between the start and end times. If the end time is earlier than the start time, it assumes the end time is on the next day.
	 */
	private function calculateDuration( $start_time, $end_time ): int {
		$start = strtotime( $start_time );
		$end   = strtotime( $end_time );

		// Handle next day scenario
		if ( $end < $start ) {
			$end += 24 * 60 * 60; // Add 24 hours
		}

		return ( $end - $start ) / 60; // Return minutes
	}

	/**
	 * Generates a list of calendar events based on special days and days off.
	 *
	 * @param array $special_days An array of special day data, where each entry includes details such as id, name, start_date, and end_date.
	 * @param array $days_off An array of days off data, where each entry includes details such as id, name, start_date, end_date, and optional recurrence information.
	 *
	 * @return array A list of events, each represented as an associative array containing id, title, start, end, type, color, and optionally recurring.
	 */
	private function generateCalendarEvents( $special_days, $days_off ): array {
		$events = array();

		// Add special days as events
		foreach ( $special_days as $special_day ) {
			$events[] = array(
				'id'    => 'special_' . $special_day['id'],
				'title' => $special_day['name'],
				'start' => $special_day['start_date'],
				'end'   => $special_day['end_date'],
				'type'  => 'special',
				'color' => '#3b82f6', // Blue for special days
			);
		}

		// Add days off as events
		foreach ( $days_off as $day_off ) {
			$events[] = array(
				'id'        => 'off_' . $day_off['id'],
				'title'     => $day_off['name'],
				'start'     => $day_off['start_date'],
				'end'       => $day_off['end_date'],
				'type'      => 'day_off',
				'color'     => '#ef4444', // Red for days off
				'recurring' => $day_off['recurring'] ?? 0,
			);
		}

		return $events;
	}

	/**
	 * Retrieves the available time slots for a specific employee, service, and date, optionally considering a specific location.
	 *
	 * @param int|string $employee_id The ID of the employee whose availability is being retrieved.
	 * @param int|string $service_id The ID of the service related to the employee's availability.
	 * @param string $date The date for which the time slots are being retrieved (formatted as YYYY-MM-DD).
	 * @param int|string|null $location_id Optional. The ID of the location to filter the availability by.
	 *
	 * @return array An associative array containing the employee's ID and their schedule, including available time slots for the provided date.
	 */
	public function getEmployeeAvailableTimeSlot( $employee_id, $service_id, $date, $location_id = null ) {
		$assigned_services = $this->userRepo->getUserServices( $employee_id );
		$matched_services  = array_filter(
			$assigned_services,
			function ( $srv ) use ( $service_id ) {
				return $srv['service_id'] == $service_id;
			}
		);

		$matched_services = array_values( $matched_services );

		$working_hours = UserWeekdayDataService::getEffectiveWeekdays( $employee_id );
		$special_days  = UserWeekdayDataService::getEffectiveSpecialDays( $employee_id );
		$days_off      = UserWeekdayDataService::getEffectiveDaysOff( $employee_id );
		$data          = array(
			'employee_id' => $employee_id,
			'date'        => $date,
			'service_id'  => $service_id,
			'service'     => ! empty( $matched_services ) ? $matched_services[0] : null,
		);

		return array(
			'employee_id' => $employee_id,
			'schedule'    => array(
				'available_dates' => $this->generateAvailableTimeSlot( $working_hours, $special_days, $days_off, $data, $location_id ),
			),
		);
	}


	/**
	 * Generates a list of available time slots for scheduling based on working hours, special days, and days off.
	 *
	 * @param array $working_hours An array containing the regular working hours for each day of the week.
	 * @param array $special_days An array of special days with specific time periods and overrides for availability.
	 * @param array $days_off An array of dates that are marked as days off.
	 * @param array $data An associative array containing additional data such as the target start date.
	 * @param int|string $location_id The ID of the location where the schedule is being generated.
	 * @param int $days_ahead The number of days to calculate availability ahead from the start date. Defaults to 1.
	 *
	 * @return array An array of associative arrays where each entry represents an available date with its corresponding time slots,
	 *               availability status, and any special day information.
	 */
	private function generateAvailableTimeSlot( $working_hours, $special_days, $days_off, $data, $location_id, $days_ahead = 1 ): array {
		$available_dates = array();
		$start_date      = $data['date'] ? new DateTime( $data['date'] ) : new DateTime();
		$end_date        = clone $start_date;
		// Create maps for quick lookup
		$days_off_map      = $this->createDaysOffMap( $days_off );
		$working_hours_map = $this->createWorkingHoursMap( $working_hours );
		$special_days_map  = $this->createSpecialDaysMap( $special_days );

		$current_date = clone $start_date;
		while ( $current_date <= $end_date ) {
			$date_str  = $current_date->format( 'Y-m-d' );
			$day_index = (int) $current_date->format( 'N' ); // 1 = Monday, 7 = Sunday

			// Skip if it's a day off
			if ( isset( $days_off_map[ $date_str ] ) ) {
				$current_date->add( new DateInterval( 'P1D' ) );
				continue;
			}

			// Check for special days first (they override regular schedule)
			if ( isset( $special_days_map[ $date_str ] ) ) {
				$special_day = $special_days_map[ $date_str ];
				if ( ! empty( $special_day['time_periods'] ) ) {
					$available_dates[] = array(
						'date'         => $date_str,
						'day_name'     => $current_date->format( 'l' ),
						'time_slots'   => $this->formatTimeSlots( $special_day['time_periods'], $data, $location_id ),
						'is_available' => true,
						'special_day'  => $special_day['name'],
					);
				}
			} elseif ( isset( $working_hours_map[ $day_index ] ) && ! empty( $working_hours_map[ $day_index ]['time_periods'] ) ) {
				$available_dates[] = array(
					'date'         => $date_str,
					'day_name'     => $current_date->format( 'l' ),
					'time_slots'   => $this->formatTimeSlots( $working_hours_map[ $day_index ]['time_periods'], $data, $location_id, $working_hours_map[ $day_index ]['time_breaks'] ),
					'is_available' => true,
				);
			}

			$current_date->add( new DateInterval( 'P1D' ) );
		}

		return $available_dates;
	}

	/**
	 * Formats and generates time slots based on given time periods, service details, location, and constraints such as time breaks.
	 *
	 * @param array $time_periods Array of time periods where each period contains start and end times, associated locations, and services.
	 * @param array $data Array containing service and employee details, including service ID, employee ID, and availability date.
	 * @param int|string $location_id The ID of the location for which time slots should be generated.
	 * @param array $time_breaks Optional. Array of time breaks, where each break has a start time and end time. Defaults to an empty array.
	 * @param int $interval Optional. The time slot duration in minutes. Defaults to 30 minutes.
	 *
	 * @return array An array of formatted time slots, each including start time, end time, duration, location details, service details, current and maximum capacity, and availability status.
	 */
	private function formatTimeSlots( $time_periods, $data, $location_id, $time_breaks = array(), $interval = 30 ): array {
		$serviceInfo = Service::find( $data['service_id'] );
		if ( $serviceInfo->get_id() && $serviceInfo->get_duration() > 0 ) {
			$interval = $serviceInfo->get_duration() / 60;
		}
		$buffer_after    = $serviceInfo->get_buffer_after() / 60 ?? 0;
		$buffer_before   = $serviceInfo->get_buffer_before() / 60 ?? 0;
		$formatted_slots = array();
		foreach ( $time_periods as $period ) {
			if ( $period['start_time'] === $period['end_time'] ) {
				continue;
			}
			if ( rtrb_pro_active() && ! empty( $period['locations'] ) ) {
				$location_ids = array_column( $period['locations'], 'location_id' );
				if ( ! in_array( $location_id, $location_ids ) ) {
					continue; // Skip this period if location doesn't match
				}
			}
			if ( ! empty( $period['services'] ) ) {
				$service_ids = array_column( $period['services'], 'service_id' );
				if ( ! in_array( $data['service_id'], $service_ids ) ) {
					continue; // Skip this period if location doesn't match
				}
			}
			$start = new DateTime( $period['start_time'] );
			$end   = new DateTime( $period['end_time'] );

			while ( $start < $end ) {
				// Display start = actual start + buffer_before
				$display_start = ( clone $start )->modify( "+{$buffer_before} minutes" );
				// Display end = display_start + interval
				$display_end = ( clone $display_start )->modify( "+{$interval} minutes" );

				if ( $display_end > $end ) {
					break;
				}

				// Check breaks
				$is_break = false;
				foreach ( $time_breaks as $break ) {
					$break_start = new DateTime( $break['start_time'] );
					$break_end   = new DateTime( $break['end_time'] );

					if ( $display_start < $break_end && $display_end > $break_start ) {
						$is_break = true;
						break;
					}
				}

				if ( ! $is_break ) {
					// Check conflicts: from actual start to (display_end + buffer_after)
					$conflict_end = ( clone $display_end )->modify( "+{$buffer_after} minutes" );

					$conflictingAppointments = $this->appointmentRepo->getAppointmentsInTimeRange(
						$data['service_id'],
						$data['employee_id'],
						$data['date'],
						$start->format( 'H:i:s' ),
						$conflict_end->format( 'H:i:s' )
					);

					$current_capacity = count( $conflictingAppointments );
					$max_capacity     = $data['service']['max_capacity'] ?? 1;
					$is_available     = $current_capacity < $max_capacity;

					$formatted_slots[] = array(
						'start_time'       => $display_start->format( 'H:i:s' ),
						'end_time'         => $display_end->format( 'H:i:s' ),
						'duration_minutes' => $interval,
						'locations'        => $period['locations'] ?? array(),
						'services'         => $period['services'] ?? array(),
						'current_capacity' => $current_capacity,
						'max_capacity'     => $max_capacity,
						'is_available'     => $is_available,
					);
				}

				// Next slot = current display_end + buffer_after
				$start = ( clone $display_end )->modify( "+{$buffer_after} minutes" );
			}
		}

		return $formatted_slots;
	}

	/**
	 * Updates the payment details for a specific appointment.
	 *
	 * @param int|string $id The ID of the payment to be updated.
	 * @param string $payment_status The new status of the payment (e.g., 'paid', 'partial_payment', 'refunded').
	 * @param string|null $transaction_id The transaction ID associated with the payment.
	 * @param float|int $amount The payment amount to be updated.
	 * @param array $appointment The appointment details containing payment and service information.
	 *
	 * @return bool True if the payment was successfully updated, false otherwise.
	 */
	public function updatePayment( $id, $payment_status, $transaction_id, $amount, $appointment ) {
		$payment_id = $appointment['payment']['id'] ?? null;
		if ( ! $payment_id ) {
			return false;
		}

		if ( $appointment['payment']['payment_method'] !== 'offline' ) {
			$transaction_id = $appointment['payment']['transaction_id'] ?? null;
		}
		$previous_amount = $appointment['payment']['amount'] ?? 0;
		$total_amount    = $appointment['payment']['total_amount'] ?? 0;
		$servicePrice    = $appointment['customer']['price'] ?? 0;

		// Adjust amount based on status
		if ( $payment_status === 'refunded' ) {
			$amount = $previous_amount - $amount; // Refund sets the amount to previous
		} else {
			$amount = $previous_amount + $amount; // Add new payment
		}
		// Determine partial payment
		if ( $payment_status === 'paid' && ( $total_amount > $amount ) ) {
			$payment_status = 'partial_payment';
		}
		// Determine partial payment
		if ( $payment_status === 'partial_payment' && ( $total_amount <= $amount ) ) {
			$payment_status = 'paid';
		}

		$paymentData = array(
			'status'         => $payment_status,
			'transaction_id' => $transaction_id,
			'amount'         => $amount,
			'due_amount'     => $total_amount - $amount,
		);
		// Set paid_at only for full paid
		if ( $payment_status === 'paid' ) {
			$paymentData['paid_at'] = gmdate( 'Y-m-d H:i:s' );
		}

		// Optionally clear paid_at for refunded
		if ( $payment_status === 'refunded' ) {
			$paymentData['paid_at'] = null;
		}

		return $this->paymentRepo->updatePayment( $payment_id, $paymentData );
	}

	/**
	 * Retrieve appointments for the calendar view with optional filtering.
	 *
	 * Supports view-based date boundaries (day, week, month, list, timeline)
	 * combined with an optional date_range filter (today or custom range)
	 * that narrows results within the view's boundaries.
	 *
	 * @param string|int $service_id  Comma-separated service IDs or empty.
	 * @param string|int $customer_id Customer ID (reserved for future use).
	 * @param string|int $employee_id Employee ID or 'all'.
	 * @param string     $date        Reference date for the calendar view.
	 * @param string     $date_range  Optional filter: 'today' or 'Y-m-d:Y-m-d' custom range.
	 * @param string     $view        Calendar view: day, week, month, list, timeline.
	 *
	 * @return mixed
	 */
	public function getAppointmentForCalender( $service_id, $customer_id, $employee_id, $date, $date_range, $view ) {

		$appointment = Appointment::query();

		// Filter by service IDs if provided.
		if ( ! empty( $service_id ) ) {
			$service_ids = explode( ',', $service_id );
			if ( $service_ids ) {
				$appointment = $appointment->whereIn( 'service_id', $service_ids );
			}
		}

		// Filter by employee unless viewing all.
		if ( $employee_id !== 'all' ) {
			$appointment = $appointment->where( 'employee_id', $employee_id );
		}

		// Determine date boundaries based on view type.
		if ( $date ) {
			$timestamp = strtotime( $date );

			switch ( $view ) {
				case 'day':
					$view_start = gmdate( 'Y-m-d', $timestamp );
					$view_end   = $view_start;
					break;

				case 'week':
					$view_start = gmdate( 'Y-m-d', strtotime( 'monday this week', $timestamp ) );
					$view_end   = gmdate( 'Y-m-d', strtotime( 'sunday this week', $timestamp ) );
					break;

				case 'month':
				case 'list':
				case 'timeline':
					$view_start = gmdate( 'Y-m-01', $timestamp );
					$view_end   = gmdate( 'Y-m-t', $timestamp );
					break;

				default:
					$view_start = gmdate( 'Y-m-d', $timestamp );
					$view_end   = null;
					break;
			}

			// Apply date_range filter within the view boundaries.
			if ( ! empty( $date_range ) ) {
				$filtered = $this->resolveDateRange( $date_range );

				if ( $filtered ) {
					// Clamp the filter within the view boundaries.
					$final_start = $filtered['start'];
					$final_end   = $filtered['end'];

					if ( $view_end ) {
						// Ensure filter doesn't exceed view boundaries.
						if ( $final_start < $view_start ) {
							$final_start = $view_start;
						}
						if ( $final_end > $view_end ) {
							$final_end = $view_end;
						}
					}

					if ( $final_start === $final_end ) {
						$appointment = $appointment->whereDate( 'appointment_date', '=', $final_start );
					} else {
						$appointment = $appointment->whereBetween(
							'appointment_date',
							array( $final_start, $final_end )
						);
					}
				}
			} else {
				// No date_range — use full view boundaries.
				if ( $view_end && $view_start === $view_end ) {
					$appointment = $appointment->whereDate( 'appointment_date', '=', $view_start );
				} elseif ( $view_end ) {
					$appointment = $appointment->whereBetween(
						'appointment_date',
						array( $view_start, $view_end )
					);
				} else {
					$appointment = $appointment->where( 'appointment_date', '>=', $view_start );
				}
			}
		}

		$appointment = $appointment->orderBy( 'appointment_date', 'asc' );

		return $appointment->get();
	}

	/**
	 * Resolve a date_range string into start and end dates.
	 *
	 * Accepts 'today' for the current date or a custom range
	 * in 'Y-m-d:Y-m-d' format.
	 *
	 * @param string $date_range The date range string to resolve.
	 *
	 * @return array|null Array with 'start' and 'end' keys, or null if invalid.
	 */
	private function resolveDateRange( $date_range ) {

		if ( 'today' === $date_range ) {
			$today = gmdate( 'Y-m-d' );
			return array(
				'start' => $today,
				'end'   => $today,
			);
		}

		if ( strpos( $date_range, ':' ) !== false ) {
			$parts = explode( ':', $date_range );

			if ( count( $parts ) === 2 ) {
				$start = sanitize_text_field( $parts[0] );
				$end   = sanitize_text_field( $parts[1] );

				// Validate date formats.
				if ( strtotime( $start ) && strtotime( $end ) ) {
					return array(
						'start' => $start,
						'end'   => $end,
					);
				}
			}
		}

		return null;
	}

	/**
	 * Provides a daily overview for the dashboard, including appointment statistics, revenue details, and time slot information.
	 *
	 * @param string $day_date The date for which the daily overview is generated, formatted as 'Y-m-d'.
	 *
	 * @return array An associative array containing:
	 *               - `totalAppointment` (int): Total appointments for the day.
	 *               - `totalConfirm` (int): Total confirmed appointments for the day.
	 *               - `revenuePending` (float): Total pending revenue for the day.
	 *               - `totalPending` (string): Formatted string of the total pending amount for customers.
	 *               - `revenueCollected` (float): Total revenue collected for the day.
	 *               - `todayStats` (array): Statistics including appointments, revenue, and customers.
	 *               - `timeSlots` (array): List of time slots with appointment details such as time, status, customer, and service.
	 *               - `currency_symbol` (string): The symbol of the currency being used.
	 */
	public function dailyOverviewForDashboard( $day_date ) {
		$appointments          = $this->appointmentRepo->getAppointmentsByDate( $day_date );
		$appointmentCollection = $this->appointmentRepo->getAppointmentsWithResource( $appointments );
		$totalAppointments     = $this->appointmentRepo->getDailyAppointmentsCount( $day_date );
		$totalConfirmed        = $this->appointmentRepo->getDailyConfirmedAppointmentsCount( $day_date );

		$revenueCollected = $this->paymentRepo->getDailyCollectedRevenue( $day_date );
		$revenuePending   = $this->paymentRepo->getDailyPendingRevenue( $day_date );

		$collection         = new Collection( $appointmentCollection );
		$totalCustomers     = $collection->pluckWith( 'customer.customer_id' )->unique()->count();
		$totalPendingAmount = $collection->sum( 'customer.price' );

		$todayStats = array(
			array(
				'label'      => 'Appointments',
				'value'      => (string) $totalConfirmed,
				'total'      => (string) $totalAppointments,
				'percentage' => $totalAppointments > 0
					? round( ( $totalConfirmed / $totalAppointments ) * 100 )
					: 0,
				'icon'       => 'RiCalendarLine',
				'color'      => 'text-blue-600',
			),
			array(
				'label'      => 'Revenue',
				'value'      => number_format( $revenueCollected, 2 ),
				'total'      => get_rtrb_currency_symbol() . number_format( $totalPendingAmount, 2 ),
				'percentage' => $totalPendingAmount > 0
					? round( ( $revenueCollected / $totalPendingAmount ) * 100 )
					: 0,
				'icon'       => 'RiMoneyDollarCircleLine',
				'color'      => 'text-emerald-600',
			),
			array(
				'label'      => 'Customers',
				'value'      => (string) $totalCustomers,
				'total'      => (string) $totalCustomers, // or daily capacity if you track it
				'percentage' => 100, // adjust if you want capacity-based
				'icon'       => 'RiUserLine',
				'color'      => 'text-purple-600',
			),
		);

		// ✅ Time slots array (like timeSlots in React)
		$timeSlots = $collection->map(
			function ( $apt ) {
				return array(
					'time'     => gmdate( 'g:i A', strtotime( $apt['start_time'] ) ),
					'status'   => $apt['status'],
					'customer' => $apt['customer']->customer_info['name'] ?? null,
					'service'  => $apt['service_name'] ?? null,
					'id'       => $apt['id'],
				);
			}
		)->all(); // Use all() to get the array

		return array(
			'totalAppointment' => $totalAppointments,
			'totalConfirm'     => $totalConfirmed,
			'revenuePending'   => $revenuePending,
			'totalPending'     => number_format( $totalPendingAmount, 2 ),
			'revenueCollected' => $revenueCollected,
			'todayStats'       => $todayStats,
			'timeSlots'        => $timeSlots,
			'currency_symbol'  => get_rtrb_currency_symbol(),
		);
	}

	// Update isServiceAvailable to return detailed status

	/**
	 * Determines whether a service is available for booking based on provided criteria such as timing, capacity, and advance booking rules.
	 *
	 * @param array $data An associative array of booking details, where:
	 *                    - 'service_id' (int|string): The ID of the service to check availability for.
	 *                    - 'employee_id' (int|string|null): The ID of the employee offering the service (optional).
	 *                    - 'appointment_date' (string): The date of the intended appointment in 'YYYY-MM-DD' format.
	 *                    - 'start_time' (string): The start time of the intended appointment (e.g., 'HH:mm').
	 *                    - 'end_time' (string): The end time of the intended appointment (e.g., 'HH:mm').
	 *
	 * @return array An associative array with the following keys:
	 *               - 'available' (bool): Indicates whether the service is available (true) or not (false).
	 *               - 'reason' (string|null): The reason why the service is unavailable (e.g., 'past_time', 'minimum_time', 'too_far_ahead', 'capacity_full'), or null if available.
	 *               - 'message' (string|null): A detailed explanation of the result, suitable for end-user communication.
	 */
	public function isServiceAvailable( array $data ): array {
		$serviceInfo    = Service::find( $data['service_id'] );
		$minBookingTime = (float) $serviceInfo->get_min_booking_time(); // safely convert to float or int

		$minimum_time        = $minBookingTime / 60;
		$advance_period_days = $serviceInfo->get_booking_advance_period(); // in days
		$employee_id         = $data['employee_id'] ?? null;
		$employee            = $employee_id ? $this->userRepo->get_profile( $employee_id ) : null;

		// Get timezone - handle both string identifier and wp_timezone object
		if ( $employee && ! empty( $employee->timezone ) ) {
			// Employee has timezone like "Asia/Dhaka"
			$timezone = new DateTimeZone( $employee->timezone );
		} else {
			// wp_timezone() returns DateTimeZone object
			$timezone = wp_timezone();
		}

		// Current time in the specified timezone
		$currentDateTime = new DateTime( 'now', $timezone );

		// Appointment time in the specified timezone
		$appointmentDateTime = new DateTime(
			$data['appointment_date'] . ' ' . $data['start_time'],
			$timezone
		);

		$currentTimestamp     = $currentDateTime->getTimestamp();
		$appointmentTimestamp = $appointmentDateTime->getTimestamp();

		// Check 1: Past time
		if ( $appointmentTimestamp <= $currentTimestamp ) {
			return array(
				'available' => false,
				'reason'    => 'past_time',
				'message'   => 'The selected time has already passed. Please choose a future date and time.',
			);
		}

		// Check 2: Minimum booking time
		$minimumRequiredTime = clone $currentDateTime;
		$minimumRequiredTime->modify( "+{$minimum_time} minutes" );
		$minimumRequiredTimestamp = $minimumRequiredTime->getTimestamp();

		if ( $appointmentTimestamp < $minimumRequiredTimestamp ) {
			$hours    = floor( $minimum_time / 60 );
			$mins     = $minimum_time % 60;
			$timeText = $hours > 0 ? "{$hours} hour" . ( $hours > 1 ? 's' : '' ) : '';
			$timeText .= $mins > 0 ? ( $timeText ? ' and ' : '' ) . "{$mins} minute" . ( $mins > 1 ? 's' : '' ) : '';

			return array(
				'available' => false,
				'reason'    => 'minimum_time',
				'message'   => "This service requires at least {$timeText} advance booking. Please select a later time.",
			);
		}

		// Check 3: Maximum advance booking period
		if ( $advance_period_days > 0 ) {
			$maximumAllowedTime = clone $currentDateTime;
			$maximumAllowedTime->modify( "+{$advance_period_days} days" );
			$maximumAllowedTimestamp = $maximumAllowedTime->getTimestamp();

			if ( $appointmentTimestamp > $maximumAllowedTimestamp ) {
				$months = floor( $advance_period_days / 30 );
				$days   = $advance_period_days % 30;

				$periodText = '';
				if ( $months > 0 ) {
					$periodText = "{$months} month" . ( $months > 1 ? 's' : '' );
				}
				if ( $days > 0 ) {
					$periodText .= ( $periodText ? ' and ' : '' ) . "{$days} day" . ( $days > 1 ? 's' : '' );
				}

				return array(
					'available' => false,
					'reason'    => 'too_far_ahead',
					'message'   => "Bookings can only be made up to {$periodText} in advance. Please select an earlier date.",
				);
			}
		}

		// Check 4: Capacity
		$alreadyAppointedSlots = $this->appointmentRepo->getAppointmentsInTimeRange(
			$data['service_id'],
			$data['employee_id'],
			$data['appointment_date'],
			$data['start_time'],
			$data['end_time'],
		);

		$currentCapacity = count( $alreadyAppointedSlots );
		$maxCapacity     = $serviceInfo->max_capacity();

		if ( $currentCapacity >= $maxCapacity ) {
			return array(
				'available' => false,
				'reason'    => 'capacity_full',
				'message'   => sprintf(
					'The selected time slot is fully booked (%d/%d). Please choose a different time.',
					$currentCapacity,
					$maxCapacity
				),
			);
		}

		return array(
			'available' => true,
			'reason'    => null,
			'message'   => null,
		);
	}

	/**
	 * Retrieves the dashboard data summarizing appointment statuses.
	 *
	 * @return array An associative array containing the total count of appointments, as well as the counts for completed,
	 *               pending, cancelled, and confirmed appointments.
	 */
	public function getDashboardData( $booking_type = null ) {
		$total = $this->appointmentRepo->count( $booking_type );

		$completed = $this->appointmentRepo->countByStatus( 'completed', $booking_type );
		$pending   = $this->appointmentRepo->countByStatus( 'pending', $booking_type );
		$cancelled = $this->appointmentRepo->countByStatus( 'cancelled', $booking_type );
		$confirmed = $this->appointmentRepo->countByStatus( 'confirmed', $booking_type );

		return array(
			'total'     => $total,
			'completed' => $completed,
			'pending'   => $pending,
			'cancelled' => $cancelled,
			'confirmed' => $confirmed,
		);
	}

	/**
	 * Creates a payment intent using the specified parameters.
	 *
	 * @param array $params An associative array containing the following keys:
	 *                      - 'amount' (float|string, required): The payment amount.
	 *                      - 'currency' (string, optional): The currency for the payment (default is 'usd').
	 *                      - 'description' (string, optional): A description for the payment intent (default is 'Booking Payment').
	 *                      - 'customer_email' (string, optional): The customer's email address.
	 *                      - 'metadata' (array, optional): Additional metadata to be included in the payment intent.
	 *                      - 'service_id' (int|string, optional): The ID of the service associated with the payment.
	 *                      - 'location_id' (int|string, optional): The ID of the location associated with the payment.
	 *                      - 'employee_id' (int|string, optional): The ID of the employee associated with the payment.
	 *                      - 'customer_name' (string, optional): The name of the customer associated with the payment.
	 *
	 * @return array An associative array containing the result of the payment intent creation. Possible keys:
	 *               - 'error' (bool): Indicates if an error occurred.
	 *               - 'message' (string): A message detailing the result or error.
	 *               - 'clientSecret' (string, optional): The client secret for the payment intent, if successful.
	 *               - 'intentId' (string, optional): The ID of the created payment intent.
	 *               - 'amount' (float, optional): The payment amount.
	 *               - 'currency' (string, optional): The currency of the payment.
	 */
	public function createPaymentIntend( array $params ): array {
		try {
			// Validate required parameters
			if ( empty( $params['amount'] ) ) {
				return array(
					'error'   => true,
					'message' => 'Amount is required',
				);
			}

			// Prepare intent data
			$intentData = array(
				'amount'      => floatval( $params['amount'] ),
				'currency'    => $params['currency'] ?? 'usd',
				'description' => $params['description'] ?? 'Booking Payment',
				'metadata'    => array(),
			);

			// Add customer email if provided
			if ( ! empty( $params['customer_email'] ) ) {
				$intentData['customer_email'] = sanitize_email( $params['customer_email'] );
			}

			// Add metadata
			$metadata = array();
			if ( ! empty( $params['metadata'] ) ) {
				$metadata = $params['metadata'];
			}

			// Add additional metadata from params
			if ( ! empty( $params['service_id'] ) ) {
				$metadata['service_id'] = $params['service_id'];
			}
			if ( ! empty( $params['location_id'] ) ) {
				$metadata['location_id'] = $params['location_id'];
			}
			if ( ! empty( $params['employee_id'] ) ) {
				$metadata['employee_id'] = $params['employee_id'];
			}
			if ( ! empty( $params['customer_name'] ) ) {
				$metadata['customer_name'] = sanitize_text_field( $params['customer_name'] );
			}

			$intentData['metadata'] = $metadata;
			// Create payment intent using PaymentService
			$result = PaymentService::createPaymentIntent( 'stripe', $intentData );

			if ( ! $result['success'] ) {
				return array(
					'error'   => true,
					'message' => $result['message'] ?? 'Failed to create payment intent',
				);
			}

			// Return the client secret and other data
			return array(
				'error'        => false,
				'clientSecret' => $result['client_secret'],
				'intentId'     => $result['intent_id'],
				'amount'       => $result['amount'],
				'currency'     => $result['currency'],
				'message'      => $result['message'],
			);

		} catch ( \Exception $e ) {
			error_log( 'Payment Intent Creation Error: ' . $e->getMessage() );

			return array(
				'error'   => true,
				'message' => 'An error occurred while creating payment intent: ' . $e->getMessage(),
			);
		}
	}

	/**
	 * Verify Payment Intent after frontend confirmation
	 *
	 * @param string $paymentIntentId Payment Intent ID
	 *
	 * @return array Verification result
	 */
	public function verifyPaymentIntent( string $paymentIntentId ): array {
		try {
			$result = PaymentService::verifyPayment( 'stripe', $paymentIntentId );

			if ( ! $result['success'] ) {
				return array(
					'error'   => true,
					'message' => $result['message'] ?? 'Payment verification failed',
				);
			}

			return array(
				'error'         => false,
				'verified'      => true,
				'transactionId' => $result['data']['transaction_id'],
				'status'        => $result['data']['status'],
				'amount'        => $result['data']['amount'],
				'currency'      => $result['data']['currency'],
			);

		} catch ( \Exception $e ) {
			return array(
				'error'   => true,
				'message' => 'Payment verification error: ' . $e->getMessage(),
			);
		}
	}

	/**
	 * Retrieves configuration settings for various payment and currency options.
	 *
	 * @return array An associative array containing configuration details, including currency, Stripe payment settings
	 *               (e.g., publishable key), and PayPal payment settings (e.g., client ID).
	 */
	public function getConfig() {
		return array(
			'currency' => get_rtrb_currency(),
			'stripe'   => array(
				'publishableKey' => PaymentService::getStripePublishableKey(),
			),
			'paypal'   => array(
				'clientId' => PaymentService::getPaypalClientID(),
			),
		);
	}

	/**
	 * Create PayPal Order
	 *
	 * @param array $params Order parameters
	 *
	 * @return array
	 */
	public function createPayPalOrder( array $params ): array {
		try {
			$orderData = array(
				'amount'         => $params['amount'] ?? 0,
				'currency'       => $params['currency'] ?? 'USD',
				'description'    => $params['description'] ?? 'Booking Payment',
				'customer_email' => $params['customer_email'] ?? '',
				'metadata'       => $params['metadata'] ?? array(),
				'return_url'     => home_url( '/booking-success' ),
				'cancel_url'     => home_url( '/booking-cancel' ),
			);

			$result = PaymentService::createPayPalOrder( $orderData );

			if ( ! $result['success'] ) {
				return array(
					'error'   => true,
					'message' => $result['message'] ?? 'Failed to create PayPal order',
				);
			}

			return $result;

		} catch ( \Exception $e ) {
			return array(
				'error'   => true,
				'message' => $e->getMessage(),
			);
		}
	}

	/**
	 * Capture PayPal Order
	 *
	 * @param string $orderId PayPal order ID
	 *
	 * @return array
	 */
	public function capturePayPalOrder( string $orderId ): array {
		try {
			$result = PaymentService::capturePayPalOrder( $orderId );

			if ( ! $result['success'] ) {
				return array(
					'error'   => true,
					'message' => $result['message'] ?? 'Failed to capture PayPal order',
				);
			}

			return $result;

		} catch ( \Exception $e ) {
			return array(
				'error'   => true,
				'message' => $e->getMessage(),
			);
		}
	}


	public function fireStatusAction( $appointment ) {
		do_action( 'radius_booking_appointment_status_' . $appointment->get_status(), $appointment->get_id() );
	}

	/**
	 * Execute the status change based on the requested status.
	 *
	 * @param AppointmentService $service The appointment service.
	 * @param int                $id      The appointment ID.
	 * @param string             $status  The new status.
	 * @param WP_REST_Request    $request The REST request object.
	 * @return bool|WP_Error True on success, WP_Error on invalid status.
	 */
	public function executeStatusChange( AppointmentService $service, int $id, string $status, $request ) {
		switch ( $status ) {
			case 'confirmed':
				return $service->confirmAppointment( $id );

			case 'completed':
				return $service->completeAppointment( $id );

			case 'pending':
				return $service->pendingAppointment( $id );

			case 'cancelled':
				$reason = $request->get_param( 'reason' );
				return $service->cancelAppointment( $id, $reason );

			default:
				return new WP_Error( 'invalid_status', 'Invalid status' );
		}
	}
}
