<?php

namespace SugarCalendarBookings\Availability;

use DateTimeImmutable;
use DateTimeZone;

/**
 * Working schedule rules map.
 *
 * @since 1.0.0
 */
class WorkingScheduleRulesMap {

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

	/**
	 * 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;

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

	/**
	 * Matched rules cache.
	 *
	 * @since 1.0.0
	 *
	 * @var array
	 */
	private $cache = [];

	/**
	 * 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;
		$this->from_date = $from_date;
		$this->to_date   = $to_date;
	}

	/**
	 * Get matching rules for a given date.
	 *
	 * @since 1.0.0
	 * @since 1.4.0 Add defensive check for employee id retrieval.
	 *
	 * @param DateTimeImmutable $date Date in UTC.
	 *
	 * @return array<AbstractWorkingScheduleRule> Matching rules.
	 */
	public function get_matching_rules( $date ) { // phpcs:ignore Generic.Metrics.CyclomaticComplexity.TooHigh

		$cache_key = $date->format( 'Y-m-d' );

		if ( isset( $this->cache[ $cache_key ] ) ) {
			return $this->cache[ $cache_key ];
		}

		// Lazy load the rules.
		$this->map_rules();

		$matches = [];

		foreach ( $this->rules_map as $time_zone => $rule_set ) {
			$local_date = $date->setTimezone( new DateTimeZone( $time_zone ) );

			$weekday     = $local_date->format( 'N' );
			$date_string = $local_date->format( 'Y-m-d' );

			$rules = array_merge(
				$rule_set['weekday'][ $weekday ] ?? [],
				$rule_set['date_range'][ $date_string ] ?? []
			);

			$highest_priority_rules = [];

			foreach ( $rules as $rule ) {
				$employee    = $rule->get_employee();
				$employee_id = $employee ? $employee->id : 0;

				if (
					! isset( $highest_priority_rules[ $employee_id ] ) ||
					$rule->get_priority() > $highest_priority_rules[ $employee_id ]->get_priority()
				) {
					$highest_priority_rules[ $employee_id ] = $rule;
				}
			}

			$matches = array_merge(
				$matches,
				array_values( $highest_priority_rules )
			);
		}

		$this->cache[ $cache_key ] = $matches;

		return $matches;
	}

	/**
	 * Whether the rules have matching rules for a given date.
	 *
	 * @since 1.0.0
	 *
	 * @param DateTimeImmutable $date Date in UTC.
	 *
	 * @return bool
	 */
	public function has_matching_rules( $date ) {

		return ! empty( $this->get_matching_rules( $date ) );
	}

	/**
	 * Map rules for faster lookup.
	 *
	 * @since 1.0.0
	 */
	private function map_rules() {

		if ( $this->rules_map !== null ) {
			return;
		}

		foreach ( $this->rules as $rule ) {
			if ( $rule instanceof WorkingScheduleWeekdayRule ) {
				$this->map_weekday_rule( $rule );
			} elseif ( $rule instanceof WorkingScheduleDateRangeRule ) {
				$this->map_date_range_rule( $rule );
			}
		}

		if ( empty( $this->rules_map ) ) {
			$this->rules_map = [];
		}
	}

	/**
	 * Map weekday rule.
	 *
	 * @since 1.0.0
	 *
	 * @param WorkingScheduleWeekdayRule $rule Rule.
	 */
	private function map_weekday_rule( $rule ) {

		$time_zone = $rule->get_time_zone()->getName();
		$weekday   = $rule->get_weekday();

		if ( ! isset( $this->rules_map[ $time_zone ]['weekday'][ $weekday ] ) ) {
			$this->rules_map[ $time_zone ]['weekday'][ $weekday ] = [];
		}

		$this->rules_map[ $time_zone ]['weekday'][ $weekday ][] = $rule;
	}

	/**
	 * Map date range rule.
	 *
	 * @since 1.0.0
	 *
	 * @param WorkingScheduleDateRangeRule $rule Rule.
	 */
	private function map_date_range_rule( $rule ) {

		$time_zone  = $rule->get_time_zone()->getName();
		$rule_start = $rule->get_start_date();
		$rule_end   = $rule->get_end_date();

		// Find the intersection of the rule's range and the requested range.
		$start = $rule_start > $this->from_date ? $rule_start : $this->from_date;
		$end   = $rule_end < $this->to_date ? $rule_end : $this->to_date;

		// If the intersection is invalid, skip this rule.
		if ( $start > $end ) {
			return;
		}

		/*
		 * If date range expansion will cause a memory issue, we'll need to implement Interval Tree.
		 * For now, we'll just skip overrides that are outside the requested range and it should be fine.
		 */
		for ( $date = $start; $date <= $end; $date = $date->modify( '+1 day' ) ) {
			$date_string = $date->format( 'Y-m-d' );

			if ( ! isset( $this->rules_map[ $time_zone ]['date_range'][ $date_string ] ) ) {
				$this->rules_map[ $time_zone ]['date_range'][ $date_string ] = [];
			}

			$this->rules_map[ $time_zone ]['date_range'][ $date_string ][] = $rule;
		}
	}
}
