<?php

namespace SugarCalendarBookings\Integrations\ExternalBusyTimes;

use SugarCalendarBookings\Employees\EmployeeModel;
use SugarCalendarBookings\Integrations\AbstractOAuthIntegration;
use SugarCalendarBookings\Integrations\CalendarConflictProviderInterface;
use SugarCalendarBookings\Integrations\IntegrationCapabilityRegistry;
use SugarCalendarBookings\Integrations\OAuthRelay\OAuthConnectionModel;
use SugarCalendarBookings\Plugin;

/**
 * External busy time service.
 *
 * Handles creating, upserting, and deleting external busy time entries.
 *
 * @since 1.4.0
 */
class ExternalBusyTimeService {

	/**
	 * Cached employee ID lookups keyed by "{provider}:{user_id}".
	 *
	 * @since 1.4.0
	 * @since 1.5.0 Keyed by provider + user_id (global-connection resolution is per provider).
	 *
	 * @var array<string, int|null>
	 */
	private array $employee_id_cache = [];

	/**
	 * Store busy times from a provider for a connection.
	 *
	 * Resolves employee_id from the connection's user_id and upserts
	 * all busy time entries. Existing entries with matching provider_event_id
	 * are updated; new entries are inserted.
	 *
	 * @since 1.4.0
	 *
	 * @param OAuthConnectionModel $connection  The OAuth connection.
	 * @param string               $provider    Provider slug (e.g. 'zoom').
	 * @param string               $calendar_id Provider-specific calendar ID.
	 * @param array                $events      Array of event arrays with keys:
	 *                                          provider_event_id, title, start_at, end_at, is_all_day.
	 */
	public function store_busy_times( OAuthConnectionModel $connection, string $provider, string $calendar_id, array $events ): void {

		if ( empty( $events ) ) {
			return;
		}

		$employee_id = $this->resolve_employee_id( $connection );

		// Batch-load existing records to avoid N+1 queries.
		$event_ids    = array_column( $events, 'provider_event_id' );
		$existing_map = $this->find_existing_batch( $provider, $calendar_id, $event_ids );

		foreach ( $events as $event ) {
			$existing = $existing_map[ $event['provider_event_id'] ] ?? null;

			// Delete stale record when employee_id changed (AbstractModel::save() strips nulls).
			if ( $existing && $existing->employee_id !== $employee_id ) {
				$existing->delete();
				$existing = null;
			}

			if ( $existing ) {
				$this->update_existing( $existing, $connection->id, $event );
			} else {
				ExternalBusyTimeModel::create(
					$this->build_row( $employee_id, $connection->id, $provider, $calendar_id, $event )
				);
			}
		}
	}

	/**
	 * Upsert a single busy time entry for a meeting created by our system.
	 *
	 * Used after MeetingManager creates/updates a provider meeting to ensure
	 * the busy time is tracked for account-level conflict checking.
	 *
	 * @since 1.4.0
	 *
	 * @param OAuthConnectionModel $connection        The OAuth connection.
	 * @param string               $provider          Provider slug.
	 * @param string               $provider_event_id External event/meeting ID.
	 * @param string               $title             Event title.
	 * @param string               $start_at          Start datetime (Y-m-d H:i:s).
	 * @param string               $end_at            End datetime (Y-m-d H:i:s).
	 */
	public function upsert_meeting_busy_time(
		OAuthConnectionModel $connection,
		string $provider,
		string $provider_event_id,
		string $title,
		string $start_at,
		string $end_at
	): void {

		$employee_id = $this->resolve_employee_id( $connection );

		$this->upsert_busy_time(
			$connection->id,
			$employee_id,
			$provider,
			$connection->account_id,
			[
				'provider_event_id' => $provider_event_id,
				'title'             => $title,
				'start_at'          => $start_at,
				'end_at'            => $end_at,
				'is_all_day'        => false,
			]
		);
	}

	/**
	 * Delete a busy time by provider event ID.
	 *
	 * @since 1.4.0
	 *
	 * @param string $provider          Provider slug.
	 * @param string $provider_event_id External event/meeting ID.
	 */
	public function delete_by_provider_event( string $provider, string $provider_event_id ): void {

		ExternalBusyTimeModel::query()
			->where( 'provider', '=', $provider, '%s' )
			->where( 'provider_event_id', '=', $provider_event_id, '%s' )
			->delete();
	}

	/**
	 * Delete all busy time entries for a specific provider calendar.
	 *
	 * Called when a calendar is removed from the conflict-check selection so
	 * its entries stop blocking booking slots immediately.
	 *
	 * @since 1.5.0
	 *
	 * @param string $provider    Provider slug (e.g. 'outlook').
	 * @param string $calendar_id Provider calendar ID.
	 */
	public function delete_by_calendar( string $provider, string $calendar_id ): void {

		ExternalBusyTimeModel::query()
			->where( 'provider', '=', $provider, '%s' )
			->where( 'calendar_id', '=', $calendar_id, '%s' )
			->delete();
	}

	/**
	 * Resolve employee ID from an OAuth connection's WP user_id.
	 *
	 * Global (admin) connections always resolve to null so their busy times
	 * are stored account-level and block every employee — even when the
	 * connecting admin is also linked to an employee record. Only personal
	 * employee connections produce employee-scoped busy times.
	 *
	 * Results are cached per request to avoid repeated queries.
	 *
	 * @since 1.4.0
	 * @since 1.5.0 Global connections resolve to null (account-level).
	 *
	 * @param OAuthConnectionModel $connection The OAuth connection.
	 *
	 * @return int|null Employee ID or null for account-level.
	 */
	private function resolve_employee_id( OAuthConnectionModel $connection ): ?int {

		$cache_key = $connection->provider . ':' . (int) $connection->user_id;

		if ( array_key_exists( $cache_key, $this->employee_id_cache ) ) {
			return $this->employee_id_cache[ $cache_key ];
		}

		if ( $this->is_global_connection( $connection ) ) {
			$this->employee_id_cache[ $cache_key ] = null;

			return null;
		}

		$employee = EmployeeModel::query()
			->where( 'user_id', '=', $connection->user_id, '%d' )
			->first();

		$this->employee_id_cache[ $cache_key ] = $employee ? $employee->id : null;

		return $this->employee_id_cache[ $cache_key ];
	}

	/**
	 * Whether the connection belongs to the provider's global account owner.
	 *
	 * Delegates to the provider integration, which owns the global-account
	 * resolution rules (pure admins first, earliest admin connection otherwise).
	 *
	 * @since 1.5.0
	 *
	 * @param OAuthConnectionModel $connection The OAuth connection.
	 *
	 * @return bool
	 */
	private function is_global_connection( OAuthConnectionModel $connection ): bool {

		$integration = Plugin::get( IntegrationCapabilityRegistry::class )
			->find( CalendarConflictProviderInterface::class, (string) $connection->provider );

		return $integration instanceof AbstractOAuthIntegration
			&& $integration->is_global_connection( $connection );
	}

	/**
	 * Upsert a single busy time row.
	 *
	 * @since 1.4.0
	 *
	 * @param int      $connection_id OAuth connection ID.
	 * @param int|null $employee_id   Employee ID or null.
	 * @param string   $provider      Provider slug.
	 * @param string   $calendar_id   Calendar ID.
	 * @param array    $event         Event data.
	 */
	private function upsert_busy_time( int $connection_id, ?int $employee_id, string $provider, string $calendar_id, array $event ): void {

		$existing = $this->find_existing( $provider, $calendar_id, $event['provider_event_id'] );

		// Delete stale record when employee_id changed (AbstractModel::save() strips nulls).
		if ( $existing && $existing->employee_id !== $employee_id ) {
			$existing->delete();
			$existing = null;
		}

		// Update existing record.
		if ( $existing ) {
			$this->update_existing( $existing, $connection_id, $event );

			return;
		}

		// Create new record.
		ExternalBusyTimeModel::create(
			$this->build_row( $employee_id, $connection_id, $provider, $calendar_id, $event )
		);
	}

	/**
	 * Batch-load existing busy times by provider event IDs.
	 *
	 * @since 1.4.0
	 *
	 * @param string   $provider    Provider slug.
	 * @param string   $calendar_id Calendar ID.
	 * @param string[] $event_ids   Array of provider event IDs.
	 *
	 * @return array<string, ExternalBusyTimeModel> Keyed by provider_event_id.
	 */
	private function find_existing_batch( string $provider, string $calendar_id, array $event_ids ): array {

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

		$records = ExternalBusyTimeModel::query()
			->where( 'provider', '=', $provider, '%s' )
			->where( 'calendar_id', '=', $calendar_id, '%s' )
			->where( 'provider_event_id', 'IN', $event_ids, '%s' )
			->get();

		$map = [];

		foreach ( $records as $record ) {
			$map[ $record->provider_event_id ] = $record;
		}

		return $map;
	}

	/**
	 * Find an existing busy time by unique key.
	 *
	 * @since 1.4.0
	 *
	 * @param string $provider          Provider slug.
	 * @param string $calendar_id       Calendar ID.
	 * @param string $provider_event_id External event ID.
	 *
	 * @return ExternalBusyTimeModel|null
	 */
	private function find_existing( string $provider, string $calendar_id, string $provider_event_id ): ?ExternalBusyTimeModel {

		return ExternalBusyTimeModel::query()
			->where( 'provider', '=', $provider, '%s' )
			->where( 'calendar_id', '=', $calendar_id, '%s' )
			->where( 'provider_event_id', '=', $provider_event_id, '%s' )
			->first();
	}

	/**
	 * Update an existing busy time record.
	 *
	 * @since 1.4.0
	 *
	 * @param ExternalBusyTimeModel $existing      Existing model.
	 * @param int                   $connection_id OAuth connection ID.
	 * @param array                 $event         Event data.
	 */
	private function update_existing( ExternalBusyTimeModel $existing, int $connection_id, array $event ): void {

		$existing->oauth_connection_id = $connection_id;
		$existing->title               = $event['title'] ?? '';
		$existing->start_at            = $event['start_at'];
		$existing->end_at              = $event['end_at'];
		$existing->is_all_day          = ! empty( $event['is_all_day'] ) ? 1 : 0;

		$existing->save();
	}

	/**
	 * Build a row array for creating a new busy time record.
	 *
	 * @since 1.4.0
	 *
	 * @param int|null $employee_id   Employee ID or null.
	 * @param int      $connection_id OAuth connection ID.
	 * @param string   $provider      Provider slug.
	 * @param string   $calendar_id   Calendar ID.
	 * @param array    $event         Event data.
	 *
	 * @return array Row data for ExternalBusyTimeModel::create().
	 */
	private function build_row( ?int $employee_id, int $connection_id, string $provider, string $calendar_id, array $event ): array {

		return [
			'employee_id'         => $employee_id,
			'oauth_connection_id' => $connection_id,
			'provider'            => $provider,
			'calendar_id'         => $calendar_id,
			'provider_event_id'   => $event['provider_event_id'],
			'title'               => $event['title'] ?? '',
			'start_at'            => $event['start_at'],
			'end_at'              => $event['end_at'],
			'is_all_day'          => ! empty( $event['is_all_day'] ) ? 1 : 0,
		];
	}
}
