<?php

namespace SugarCalendarBookings\Availability;

use DateTimeImmutable;

/**
 * Working periods resolver.
 *
 * @since 1.0.0
 */
class WorkingPeriodsResolver {

	/**
	 * Working schedule rules.
	 *
	 * @since 1.0.0
	 *
	 * @var array<AbstractWorkingScheduleRule>
	 */
	private $rules;

	/**
	 * Working schedule rules map.
	 *
	 * Used for fast lookup of rules that match a specific date.
	 *
	 * @since 1.0.0
	 *
	 * @var WorkingScheduleRulesMap
	 */
	private $rules_map;

	/**
	 * From date.
	 *
	 * @since 1.0.0
	 *
	 * @var DateTimeImmutable From date.
	 */
	private $from_date;

	/**
	 * To date.
	 *
	 * @since 1.0.0
	 *
	 * @var DateTimeImmutable To date.
	 */
	private $to_date;

	/**
	 * Constructor.
	 *
	 * @since 1.0.0
	 *
	 * @param array<AbstractWorkingScheduleRule> $rules     Rules.
	 * @param DateTimeImmutable                  $from_date From date.
	 * @param DateTimeImmutable                  $to_date   To date.
	 */
	public function __construct( $rules, $from_date, $to_date ) {

		$this->rules = $rules;

		/*
		 * Extend the date range by one day before and after to ensure that all slots
		 * overlapping the requested range are generated, regardless of timezone differences.
		 * This prevents missing slots that start or end near UTC day boundaries due to local time offsets.
		 */
		$this->from_date = $from_date->modify( '-1 day' );
		$this->to_date   = $to_date->modify( '+1 day' );

		$this->rules_map = new WorkingScheduleRulesMap( $rules, $this->from_date, $this->to_date );
	}

	/**
	 * Get working schedule periods for the entire date range.
	 *
	 * @since 1.0.0
	 *
	 * @return array<WorkingPeriod> Working periods.
	 */
	public function get_periods() {

		$periods = [];

		for ( $date = $this->from_date; $date <= $this->to_date; $date = $date->modify( '+1 day' ) ) {
			$periods = array_merge( $periods, $this->get_day_periods( $date ) );
		}

		return $periods;
	}

	/**
	 * Get working schedule periods for a specific date.
	 *
	 * @since 1.0.0
	 *
	 * @param DateTimeImmutable $date Date in UTC to get periods for.
	 *
	 * @return array<WorkingPeriod> Working periods.
	 */
	private function get_day_periods( $date ) {

		$periods = [];
		$rules   = [];

		if ( $this->rules_map->has_matching_rules( $date ) ) {
			$rules = $this->rules_map->get_matching_rules( $date );
		}

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

		// Convert working periods to a proper time period with a date.
		foreach ( $rules as $rule ) {
			foreach ( $rule->get_periods() as $period ) {
				$periods[] = WorkingPeriod::from_rule( $period, $date );
			}
		}

		// Merge overlapping or adjacent periods (union).
		if ( ! empty( $periods ) && count( $periods ) > 1 ) {
			$periods = $this->merge_periods_union( $periods );
		}

		return $periods;
	}

	/**
	 * Merge overlapping or adjacent periods (union).
	 *
	 * @since 1.0.0
	 *
	 * @param array<WorkingPeriodRule> $periods Array of periods to merge.
	 *
	 * @return array<WorkingPeriodRule> Merged array of periods.
	 */
	private function merge_periods_union( $periods ) {
		/*
		 * TODO: Implement proper merge logic during "Any" employee logic implementation.
		 * For now, we just merge overlapping periods with the same employee.
		 *
		 * Merge logic example. A, B, C are employees.
		 *
		 * 09:00   10:00   11:00   11:30   12:00   13:00
		 * |-------|-------|-------|-------|-------|
		 * A:  [=======]
		 * B:      [=======]
		 * C:          [=======]
		 *
		 * 09:30-10:00   [A]
		 * 10:00-10:30   [A, B]
		 * 10:30-11:00   [B, C]
		 * 11:00-11:30   [C]
		 */

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

		// Sort by start time.
		usort(
			$periods,
			function ( $a, $b ) {
				return $a->start() <=> $b->start();
			}
		);

		$merged = [];

		foreach ( $periods as $period ) {
			if ( empty( $merged ) ) {
				$merged[] = $period;

				continue;
			}

			$last_index = count( $merged ) - 1;
			$last       = $merged[ $last_index ];

			if ( $last->end() >= $period->start() ) {
				// Overlapping or adjacent, merge.
				$merged[ $last_index ] = new WorkingPeriod(
					$last->start(),
					max(
						$last->end(),
						$period->end()
					),
					$last->get_employees()
				);

			} else {
				$merged[] = $period;
			}
		}

		return $merged;
	}
}
