<?php
/**
 * MeetingAvailabilityService
 *
 * Computes available time slots for a meeting type on a given date.
 * Takes into account weekly schedule, date overrides, existing bookings,
 * buffers, min notice, max advance, and max bookings per day.
 *
 * @package RadiusTheme\RadiusBooking\Services
 * @since   1.2.0
 */

namespace RadiusTheme\RadiusBooking\Services;

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

use RadiusTheme\RadiusBooking\Helpers\SettingsHelper;
use RadiusTheme\RadiusBooking\Models\Appointment;
use RadiusTheme\RadiusBooking\Repositories\MeetingTypeRepository;
use RadiusTheme\RadiusBooking\Repositories\AppointmentRepository;

/**
 * Class MeetingAvailabilityService
 */
class MeetingAvailabilityService {

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

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

	/**
	 * MeetingAvailabilityService constructor.
	 *
	 * @param MeetingTypeRepository $meeting_repo     Meeting type repository.
	 * @param AppointmentRepository $appointment_repo Appointment repository.
	 */
	public function __construct( MeetingTypeRepository $meeting_repo, AppointmentRepository $appointment_repo ) {
		$this->meeting_repo     = $meeting_repo;
		$this->appointment_repo = $appointment_repo;
	}

	/**
	 * Get available time slots for a meeting type on a specific date.
	 *
	 * @param int    $meeting_type_id        The meeting type ID.
	 * @param string $date                   Date in Y-m-d format.
	 * @param int    $exclude_appointment_id Optional appointment ID to treat as free
	 *                                       (used by reschedule so the booking's own
	 *                                       current slot is considered available).
	 *
	 * @return array Array of available slots: [ ['start_time' => 'HH:MM', 'end_time' => 'HH:MM'], ... ]
	 */
	public function getAvailableSlots( int $meeting_type_id, string $date, int $exclude_appointment_id = 0 ): array {
		$meeting_type = $this->meeting_repo->find( $meeting_type_id );

		if ( ! $meeting_type || ! $meeting_type->is_active ) {
			return array();
		}

		// Validate date is within allowed range.
		$now             = new \DateTime( 'now', wp_timezone() );
		$target_date     = new \DateTime( $date, wp_timezone() );
		$min_notice_time = clone $now;
		$min_notice_time->modify( '+' . (int) $meeting_type->min_notice_minutes . ' minutes' );
		$max_advance_date = clone $now;
		$max_advance_date->modify( '+' . (int) $meeting_type->max_advance_days . ' days' );

		if ( $target_date > $max_advance_date ) {
			return array();
		}

		// Check date overrides.
		$overrides = $this->meeting_repo->getDateOverrides( $meeting_type_id );
		$windows   = null;

		foreach ( $overrides as $override ) {
			$override_date_val = $override->override_date ?? ( $override['override_date'] ?? null );
			if ( $override_date_val === $date ) {
				$is_unavailable = $override->is_unavailable ?? ( $override['is_unavailable'] ?? false );
				if ( $is_unavailable ) {
					return array();
				}
				// Custom hours for this date.
				$start = $override->start_time ?? ( $override['start_time'] ?? null );
				$end   = $override->end_time ?? ( $override['end_time'] ?? null );
				if ( $start && $end ) {
					$windows = array(
						array(
							'start_time' => $start,
							'end_time'   => $end,
						),
					);
				}
				break;
			}
		}

		// If no override, use weekly schedule.
		if ( null === $windows ) {
			$day_of_week  = (int) $target_date->format( 'w' ); // 0=Sun, 6=Sat
			$windows      = array();
			$availability = $this->meeting_repo->getAvailability( $meeting_type_id );

			// Use custom availability from DB if any rows exist.
			foreach ( $availability as $slot ) {
				$slot_day = $slot->day_of_week ?? ( $slot['day_of_week'] ?? null );
				if ( (int) $slot_day === $day_of_week ) {
					$windows[] = array(
						'start_time' => $slot->start_time ?? $slot['start_time'],
						'end_time'   => $slot->end_time ?? $slot['end_time'],
					);
				}
			}

			// No custom availability at all — fall back to global weekly schedule.
			if ( empty( $availability ) ) {
				$windows = $this->getGlobalScheduleWindows( $day_of_week );
			}
		}

		if ( empty( $windows ) ) {
			return array();
		}

		// Generate slots within each window.
		// All values in minutes, convert to seconds for timestamp math.
		$duration_min      = (int) $meeting_type->duration;
		$buffer_before_min = (int) $meeting_type->buffer_before;
		$buffer_after_min  = (int) $meeting_type->buffer_after;
		$slot_step_sec     = ( $buffer_before_min + $duration_min + $buffer_after_min ) * 60;
		$all_slots         = array();

		$wp_tz = wp_timezone();

		foreach ( $windows as $window ) {
			$start_dt = new \DateTime( $date . ' ' . $window['start_time'], $wp_tz );
			$end_dt   = new \DateTime( $date . ' ' . $window['end_time'], $wp_tz );

			$window_start = $start_dt->getTimestamp();
			$window_end   = $end_dt->getTimestamp();

			if ( $window_end <= $window_start ) {
				continue;
			}

			$cursor = $window_start;
			while ( ( $cursor + ( $buffer_before_min * 60 ) + ( $duration_min * 60 ) ) <= $window_end ) {
				$slot_start = $cursor + ( $buffer_before_min * 60 );
				$slot_end   = $slot_start + ( $duration_min * 60 );

				$all_slots[] = array(
					'start_time' => wp_date( 'H:i', $slot_start ),
					'end_time'   => wp_date( 'H:i', $slot_end ),
				);

				$cursor += $slot_step_sec;
			}
		}

		// Get existing bookings for this date & meeting type.
		$existing_query = Appointment::where( 'meeting_type_id', $meeting_type_id )
			->where( 'appointment_date', $date )
			->where( 'booking_type', 'meeting' )
			->where( 'status', '!=', 'cancelled' );

		if ( $exclude_appointment_id > 0 ) {
			$existing_query = $existing_query->where( 'id', '!=', $exclude_appointment_id );
		}

		$existing = $existing_query->get();

		$booked_times = array();
		foreach ( $existing as $appt ) {
			$booked_times[] = $appt->start_time ?? ( $appt['start_time'] ?? null );
		}

		// Filter out booked slots.
		$available = array_filter(
			$all_slots,
			function ( $slot ) use ( $booked_times ) {
				return ! in_array( $slot['start_time'] . ':00', $booked_times, true )
					&& ! in_array( $slot['start_time'], $booked_times, true );
			}
		);

		// Max bookings per day cap.
		$max_per_day = $meeting_type->max_bookings_per_day;
		if ( $max_per_day ) {
			$booked_count = count( $booked_times );
			$remaining    = max( 0, (int) $max_per_day - $booked_count );
			$available    = array_slice( array_values( $available ), 0, $remaining );
		}

		// Filter out past slots for today.
		if ( $target_date->format( 'Y-m-d' ) === $now->format( 'Y-m-d' ) ) {
			$min_start = $min_notice_time->format( 'H:i' );
			$available = array_filter(
				$available,
				function ( $slot ) use ( $min_start ) {
					return $slot['start_time'] >= $min_start;
				}
			);
		}

		return array_values( $available );
	}

	/**
	 * Get dates that have at least one available slot within the booking window.
	 *
	 * @param int $meeting_type_id The meeting type ID.
	 *
	 * @return array Array of date strings (Y-m-d) with available slots.
	 */
	public function getAvailableDates( int $meeting_type_id ): array {
		$meeting_type = $this->meeting_repo->find( $meeting_type_id );

		if ( ! $meeting_type || ! $meeting_type->is_active ) {
			return array();
		}

		$now       = new \DateTime( 'now', wp_timezone() );
		$max_date  = clone $now;
		$max_date->modify( '+' . (int) $meeting_type->max_advance_days . ' days' );

		$available_dates = array();
		$cursor          = clone $now;

		while ( $cursor <= $max_date ) {
			$date  = $cursor->format( 'Y-m-d' );
			$slots = $this->getAvailableSlots( $meeting_type_id, $date );

			if ( ! empty( $slots ) ) {
				$available_dates[] = $date;
			}

			$cursor->modify( '+1 day' );
		}

		return $available_dates;
	}

	/**
	 * Get time windows from the global weekly schedule for a given day.
	 *
	 * @param int $day_of_week Day index (0=Sun, 1=Mon, ..., 6=Sat).
	 *
	 * @return array Array of windows: [ ['start_time' => 'HH:MM', 'end_time' => 'HH:MM'] ]
	 */
	private function getGlobalScheduleWindows( int $day_of_week ): array {
		$day_index_to_name = array(
			0 => 'sunday',
			1 => 'monday',
			2 => 'tuesday',
			3 => 'wednesday',
			4 => 'thursday',
			5 => 'friday',
			6 => 'saturday',
		);

		$day_name = $day_index_to_name[ $day_of_week ] ?? null;
		if ( ! $day_name ) {
			return array();
		}

		$global_schedule = SettingsHelper::get_setting( 'weeklySchedule' );
		$schedule        = $global_schedule[ $day_name ] ?? null;

		if ( ! $schedule || empty( $schedule['enabled'] ) ) {
			return array();
		}

		return array(
			array(
				'start_time' => $schedule['start'] ?? '09:00',
				'end_time'   => $schedule['end'] ?? '17:00',
			),
		);
	}
}
