<?php

namespace SugarCalendarBookings\Integrations\ExternalBusyTimes;

use DateTimeImmutable;
use SugarCalendarBookings\Integrations\OAuthRelay\OAuthConnectionModel;

/**
 * External busy time conflict checker.
 *
 * Handles prefetching busy times for a date range and checking
 * employee availability against them. Used by AvailabilityService
 * to filter out employees with external calendar conflicts.
 *
 * @since 1.4.0
 */
class ExternalBusyTimeConflictChecker {

	/**
	 * Prefetch all external busy times for a date range and set of employees.
	 *
	 * Returns busy times grouped by employee ID for efficient in-memory
	 * overlap checks. Account-level entries (employee_id IS NULL)
	 * are grouped under key 0.
	 *
	 * @since 1.4.0
	 *
	 * @param array<int>           $employee_ids    Employee IDs to check.
	 * @param DateTimeImmutable    $from_date       Start date.
	 * @param DateTimeImmutable    $to_date         End date.
	 * @param array<string>        $provider_slugs  Only include entries from these providers. Empty = all.
	 * @param array<string, int[]> $global_user_ids Per-provider global account owner WP user IDs. Entries
	 *                                              from connections owned by these users are treated as
	 *                                              account-level even if stored with an employee_id (stale
	 *                                              rows from before global scoping self-heal this way).
	 *
	 * @return array<int, array<ExternalBusyTimeModel>> Busy times grouped by employee ID (0 = account-level).
	 */
	public function prefetch( array $employee_ids, DateTimeImmutable $from_date, DateTimeImmutable $to_date, array $provider_slugs = [], array $global_user_ids = [] ): array {

		$range_start = $from_date->format( 'Y-m-d' ) . ' 00:00:00';
		$range_end   = $to_date->format( 'Y-m-d' ) . ' 23:59:59';

		$query = ExternalBusyTimeModel::query()
			->join(
				'oauth_connections',
				'external_busy_times.oauth_connection_id',
				'=',
				'oauth_connections.id'
			)
			->where( 'external_busy_times.start_at', '<', $range_end, '%s' )
			->where( 'external_busy_times.end_at', '>', $range_start, '%s' )
			->where( 'oauth_connections.status', '=', 'active', '%s' );

		if ( ! empty( $provider_slugs ) ) {
			$query->where( 'external_busy_times.provider', 'IN', $provider_slugs, '%s' );
		}

		// Global-connection rows must always be fetched regardless of their
		// stored employee_id — they get reclassified as account-level below.
		$global_users_flat = array_values( array_unique( array_merge( [], ...array_values( $global_user_ids ) ) ) );

		// Fetch employee-specific + account-level + global-connection rows,
		// or skip the employee-specific set when no employee IDs are given.
		$query->where(
			function ( $q ) use ( $employee_ids, $global_users_flat ) {
				$q->where( 'external_busy_times.employee_id', 'IS NULL' );

				if ( ! empty( $employee_ids ) ) {
					$q->or_where( 'external_busy_times.employee_id', 'IN', $employee_ids, '%d' );
				}

				if ( ! empty( $global_users_flat ) ) {
					$q->or_where( 'oauth_connections.user_id', 'IN', $global_users_flat, '%d' );
				}
			}
		);

		$busy_times = $query->get();

		$connection_owners = $this->get_connection_owners( $busy_times );

		$grouped = [];

		foreach ( $busy_times as $bt ) {

			$owner_user_id = $connection_owners[ (int) $bt->oauth_connection_id ] ?? 0;
			$is_global     = in_array( $owner_user_id, $global_user_ids[ $bt->provider ] ?? [], true );

			// Employee-scoped entry from a personal (non-global) connection:
			// blocks only that employee. Global-connection entries fall through
			// to account-level routing even when stored with an employee_id —
			// rows stored before global scoping existed self-heal this way.
			if ( ! $is_global && ! empty( $bt->employee_id ) ) {
				$grouped[ (int) $bt->employee_id ][] = $bt;

				continue;
			}

			// Account-level entry: blocks every employee via group 0. Conflict
			// calendars apply to all bookings regardless of the provider's
			// employee-monitoring settings.
			$grouped[0][] = $bt;
		}

		return $grouped;
	}

	/**
	 * Map oauth_connection_id to its owner WP user_id for a set of busy times.
	 *
	 * Single batched query so global-connection classification doesn't add
	 * per-row lookups.
	 *
	 * @since 1.5.0
	 *
	 * @param array<ExternalBusyTimeModel> $busy_times Fetched busy time models.
	 *
	 * @return array<int, int> Connection ID => owner WP user ID.
	 */
	private function get_connection_owners( array $busy_times ): array {

		$connection_ids = array_values(
			array_unique(
				array_filter(
					array_map( static fn( $bt ) => (int) $bt->oauth_connection_id, $busy_times )
				)
			)
		);

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

		$connections = OAuthConnectionModel::query()
			->where( 'id', 'IN', $connection_ids, '%d' )
			->get();

		$owners = [];

		foreach ( $connections as $connection ) {
			$owners[ (int) $connection->id ] = (int) $connection->user_id;
		}

		return $owners;
	}

	/**
	 * Filter employees by pre-fetched external busy times.
	 *
	 * Account-level busy times (key 0) block all employees for the slot.
	 * Employee-specific busy times block only the matching employee.
	 *
	 * @since 1.4.0
	 *
	 * @param array $employees           Available employees.
	 * @param int   $slot_start_ts       Slot start timestamp.
	 * @param int   $slot_end_ts         Slot end timestamp.
	 * @param array $external_busy_times Pre-fetched busy times grouped by employee ID.
	 *
	 * @return array Filtered employees.
	 */
	public function filter_employees( array $employees, int $slot_start_ts, int $slot_end_ts, array $external_busy_times ): array {

		if ( empty( $employees ) || empty( $external_busy_times ) ) {
			return $employees;
		}

		// Account-level conflict blocks all employees.
		if ( $this->has_overlap( $external_busy_times[0] ?? [], $slot_start_ts, $slot_end_ts ) ) {
			return [];
		}

		return array_values(
			array_filter(
				$employees,
				fn( $employee ) => ! $employee || ! $employee->id || ! $this->has_overlap(
					$external_busy_times[ $employee->id ] ?? [],
					$slot_start_ts,
					$slot_end_ts
				)
			)
		);
	}

	/**
	 * Check if any busy time overlaps with the given time range.
	 *
	 * @since 1.4.0
	 *
	 * @param array $busy_times    Array of ExternalBusyTimeModel instances.
	 * @param int   $slot_start_ts Slot start timestamp.
	 * @param int   $slot_end_ts   Slot end timestamp.
	 *
	 * @return bool True if any busy time overlaps.
	 */
	private function has_overlap( array $busy_times, int $slot_start_ts, int $slot_end_ts ): bool {

		foreach ( $busy_times as $bt ) {
			if ( $bt->start_at->getTimestamp() < $slot_end_ts && $bt->end_at->getTimestamp() > $slot_start_ts ) {
				return true;
			}
		}

		return false;
	}
}
