<?php
/**
 * DESCRIPTION: Service for generating time slots from event availability schedules
 * and checking per-slot booking availability.
 *
 * @package NemtlyBooking\Services
 */

namespace NemtlyBooking\Services;

use NemtlyBooking\Models\Event;
use NemtlyBooking\Repositories\Booking_Repository;
use NemtlyBooking\Repositories\Event_Repository;

/**
 * Slot Service class
 *
 * @since PBOOKING_SINCE
 */
class Slot_Service {

	/**
	 * Booking repository
	 *
	 * @var Booking_Repository
	 */
	private $booking_repository;

	/**
	 * Event repository
	 *
	 * @var Event_Repository
	 */
	private $event_repository;

	/**
	 * Recurrence calculator
	 *
	 * @var Recurrence_Calculator
	 */
	private $recurrence_calculator;

	/**
	 * Slot-based event types
	 *
	 * @var array
	 */
	private const SLOT_BASED_TYPES = [ 'one-on-one', 'group' ];

	/**
	 * Day-of-week mapping from PHP's N format (1=Mon) to availability keys
	 *
	 * @var array
	 */
	private const DAY_MAP = [
		1 => 'mon',
		2 => 'tue',
		3 => 'wed',
		4 => 'thu',
		5 => 'fri',
		6 => 'sat',
		7 => 'sun',
	];

	/**
	 * Constructor
	 *
	 * @since PBOOKING_SINCE
	 *
	 * @param Booking_Repository    $booking_repository    The booking repository instance.
	 * @param Event_Repository      $event_repository      The event repository instance.
	 * @param Recurrence_Calculator $recurrence_calculator The recurrence calculator instance.
	 */
	public function __construct(
		Booking_Repository $booking_repository,
		Event_Repository $event_repository,
		Recurrence_Calculator $recurrence_calculator
	) {
		$this->booking_repository    = $booking_repository;
		$this->event_repository      = $event_repository;
		$this->recurrence_calculator = $recurrence_calculator;
	}

	/**
	 * Check if an event type uses slot-based scheduling.
	 *
	 * @since PBOOKING_SINCE
	 *
	 * @param string $event_type Event type key (e.g., 'one-on-one', 'group', 'event').
	 *
	 * @return bool
	 */
	public function is_slot_based( string $event_type ): bool {
		return in_array( $event_type, self::SLOT_BASED_TYPES, true );
	}

	/**
	 * Get available time slots for an event on a specific date.
	 *
	 * Generates slots from the event's availability schedule and cross-references
	 * them with existing bookings to determine per-slot availability.
	 *
	 * @since PBOOKING_SINCE
	 *
	 * @param int    $event_id Event ID (master event ID for recurring).
	 * @param string $date     Date string (Y-m-d).
	 *
	 * @return array Array of slot arrays with keys: start, end, available, spots_remaining, total_capacity, booking_count.
	 */
	public function get_slots_for_date( int $event_id, string $date ): array {
		$event = $this->event_repository->find( $event_id );
		if ( ! $event ) {
			return [];
		}

		// Only slot-based event types have time slots
		if ( ! $this->is_slot_based( $event->get_event_type() ) ) {
			return [];
		}

		// Determine day-of-week key for the requested date
		$day_key = $this->get_day_key( $date );
		if ( ! $day_key ) {
			return [];
		}

		// Get availability windows for this day
		$availability = $event->get_availability();
		$windows      = ! empty( $availability[ $day_key ] ) ? $availability[ $day_key ] : [];
		if ( empty( $windows ) ) {
			return [];
		}

		$duration      = $event->get_duration();
		$buffer_before = $event->get_buffer_before();
		$buffer_after  = $event->get_buffer_after();
		$max_attendees = $event->get_max_attendees();

		// Generate raw slots from all availability windows
		$raw_slots = [];
		foreach ( $windows as $window ) {
			if ( empty( $window['start'] ) || empty( $window['end'] ) ) {
				continue;
			}
			$window_slots = $this->generate_slots_from_window(
				$window['start'],
				$window['end'],
				$duration,
				$buffer_before,
				$buffer_after
			);
			$raw_slots    = array_merge( $raw_slots, $window_slots );
		}

		if ( empty( $raw_slots ) ) {
			return [];
		}

		// Fetch all existing bookings for this event+date in one query
		$existing_bookings = $this->booking_repository->find_bookings_for_date( $event_id, $date );

		// Count bookings per slot start time.
		// booking_start is stored as datetime (e.g., "2026-03-20 10:00:00"),
		// so extract just the H:i portion to match against slot start times.
		$booking_counts = [];
		foreach ( $existing_bookings as $booking ) {
			$start_raw = $booking->get_booking_start();
			if ( ! empty( $start_raw ) ) {
				// Handle both "H:i" and "Y-m-d H:i:s" formats
				$start = ( strlen( $start_raw ) > 5 ) ? gmdate( 'H:i', strtotime( $start_raw ) ) : $start_raw;
				if ( ! isset( $booking_counts[ $start ] ) ) {
					$booking_counts[ $start ] = 0;
				}
				++$booking_counts[ $start ];
			}
		}

		// Build slot response with availability info
		$slots = [];
		foreach ( $raw_slots as $slot ) {
			$booked          = isset( $booking_counts[ $slot['start'] ] ) ? $booking_counts[ $slot['start'] ] : 0;
			$is_unlimited    = 0 === $max_attendees;
			$spots_remaining = $is_unlimited ? 999 : ( $max_attendees - $booked );
			$available       = $is_unlimited || $spots_remaining > 0;

			$slots[] = [
				'start'           => $slot['start'],
				'end'             => $slot['end'],
				'available'       => $available,
				'spots_remaining' => $spots_remaining,
				'total_capacity'  => $max_attendees,
				'booking_count'   => $booked,
			];
		}

		return $slots;
	}

	/**
	 * Get upcoming bookable dates for an event.
	 *
	 * For recurring events, returns upcoming instance dates. For single events,
	 * returns the event date if it's in the future. Filters to only dates where
	 * the day-of-week has availability windows configured (for slot-based events).
	 *
	 * @since PBOOKING_SINCE
	 *
	 * @param int $event_id Event ID.
	 * @param int $limit    Max dates to return.
	 *
	 * @return array Array of date strings (Y-m-d).
	 */
	public function get_bookable_dates( int $event_id, int $limit = 30 ): array {
		$event = $this->event_repository->find( $event_id );
		if ( ! $event ) {
			return [];
		}

		$is_slot_based   = $this->is_slot_based( $event->get_event_type() );
		$availability    = $event->get_availability();
		$today           = gmdate( 'Y-m-d' );
		$candidate_dates = [];

		if ( $event->get_is_recurring() ) {
			// Generate virtual instances for the next 90 days
			$end_date  = gmdate( 'Y-m-d', strtotime( '+90 days' ) );
			$instances = $this->recurrence_calculator->generate_virtual_instances( $event_id, $today, $end_date );

			foreach ( $instances as $instance ) {
				if ( method_exists( $instance, 'is_cancelled' ) && $instance->is_cancelled() ) {
					continue;
				}
				$instance_date = method_exists( $instance, 'get_date' ) ? $instance->get_date() : ( $instance->date ?? null );

				if ( empty( $instance_date ) ) {
					continue;
				}
				if ( $instance_date < $today ) {
					continue;
				}
				$candidate_dates[] = $instance_date;
			}
		} elseif ( $is_slot_based && ! empty( $availability ) ) {
			// Non-recurring slot-based event: generate dates from the availability
			// schedule for the next 90 days. The event date is the earliest bookable
			// date, and any future day matching the availability days is valid.
			$start_date = $event->get_date();
			$start      = ( ! empty( $start_date ) && $start_date >= $today ) ? $start_date : $today;
			$end        = gmdate( 'Y-m-d', strtotime( $start . ' +90 days' ) );

			// Collect which day keys have availability windows
			$available_days = [];
			foreach ( $availability as $day_key => $windows ) {
				if ( ! empty( $windows ) ) {
					$available_days[] = $day_key;
				}
			}

			// Walk through dates and collect those matching availability days
			$current = new \DateTime( $start );
			$end_dt  = new \DateTime( $end );
			while ( $current <= $end_dt ) {
				$day_key = self::DAY_MAP[ (int) $current->format( 'N' ) ] ?? null;
				if ( $day_key && in_array( $day_key, $available_days, true ) ) {
					$candidate_dates[] = $current->format( 'Y-m-d' );
				}
				$current->modify( '+1 day' );
			}
		} else {
			// Single date-based event — use the event date if it's in the future
			$event_date = $event->get_date();
			if ( ! empty( $event_date ) && $event_date >= $today ) {
				$candidate_dates[] = $event_date;
			}
		}

		// For slot-based events, filter to only dates where the day has availability windows
		if ( $is_slot_based && ! empty( $availability ) ) {
			$candidate_dates = array_filter(
				$candidate_dates,
				function ( $date ) use ( $availability ) {
					$day_key = $this->get_day_key( $date );
					return ! empty( $availability[ $day_key ] );
				}
			);
			$candidate_dates = array_values( $candidate_dates );
		}

		return array_slice( $candidate_dates, 0, $limit );
	}

	/**
	 * Generate raw time slots from an availability window.
	 *
	 * Steps through the window by (buffer_before + duration + buffer_after) intervals
	 * and produces an array of bookable [start, end] pairs. Does not check bookings.
	 *
	 * @since PBOOKING_SINCE
	 *
	 * @param string $window_start  Start time (H:i format).
	 * @param string $window_end    End time (H:i format).
	 * @param int    $duration      Slot duration in minutes.
	 * @param int    $buffer_before Buffer minutes before each slot.
	 * @param int    $buffer_after  Buffer minutes after each slot.
	 *
	 * @return array Array of ['start' => 'H:i', 'end' => 'H:i'] pairs.
	 */
	private function generate_slots_from_window( string $window_start, string $window_end, int $duration, int $buffer_before = 0, int $buffer_after = 0 ): array {
		if ( $duration <= 0 ) {
			return [];
		}

		// Use a reference date to do time math
		$base_date = '2000-01-01 ';
		$start_ts  = strtotime( $base_date . $window_start );
		$end_ts    = strtotime( $base_date . $window_end );

		if ( false === $start_ts || false === $end_ts || $start_ts >= $end_ts ) {
			return [];
		}

		$step  = ( $buffer_before + $duration + $buffer_after ) * 60; // Convert to seconds
		$slots = [];

		$current = $start_ts;
		while ( true ) {
			$slot_start = $current + ( $buffer_before * 60 );
			$slot_end   = $slot_start + ( $duration * 60 );

			// Stop if the slot end exceeds the availability window
			if ( $slot_end > $end_ts ) {
				break;
			}

			$slots[] = [
				'start' => gmdate( 'H:i', $slot_start ),
				'end'   => gmdate( 'H:i', $slot_end ),
			];

			$current += $step;
		}

		return $slots;
	}

	/**
	 * Get the availability day key for a given date.
	 *
	 * @since PBOOKING_SINCE
	 *
	 * @param string $date Date string (Y-m-d).
	 *
	 * @return string|null Day key (mon-sun) or null if date is invalid.
	 */
	private function get_day_key( string $date ): ?string {
		$timestamp = strtotime( $date );
		if ( false === $timestamp ) {
			return null;
		}

		$day_number = (int) gmdate( 'N', $timestamp );

		return self::DAY_MAP[ $day_number ] ?? null;
	}
}
