<?php

namespace SugarCalendarBookings\Integrations\CalendarEvents;

use SugarCalendarBookings\Appointments\AppointmentModel;
use SugarCalendarBookings\Integrations\CalendarEventProviderInterface;
use SugarCalendarBookings\Integrations\IntegrationCapabilityRegistry;
use SugarCalendarBookings\Integrations\OAuthRelay\OAuthConnectionModel;

/**
 * Calendar event dispatcher.
 *
 * The only class that calls push_event / update_event / delete_event on
 * provider implementations. Catches WP_Error per dispatch and accumulates
 * errors keyed by "{provider_slug}:{connection_id}".
 *
 * @since 1.5.0
 */
class CalendarEventDispatcher {

	/**
	 * Integration capability registry.
	 *
	 * @since 1.5.0
	 *
	 * @var IntegrationCapabilityRegistry
	 */
	private IntegrationCapabilityRegistry $registry;

	/**
	 * Constructor.
	 *
	 * @since 1.5.0
	 *
	 * @param IntegrationCapabilityRegistry $registry Capability registry.
	 */
	public function __construct( IntegrationCapabilityRegistry $registry ) {

		$this->registry = $registry;
	}

	/**
	 * Execute the diff against providers and return the resulting link set + errors.
	 *
	 * @param AppointmentModel $appointment Target appointment.
	 * @param EventLinkDiff    $diff        Diff to execute.
	 *
	 * @return DispatchResult
	 * @since 1.5.0
	 */
	public function dispatch(
		AppointmentModel $appointment,
		EventLinkDiff $diff
	): DispatchResult {

		$errors = [];

		// Start from the orphan-keep set so providers not currently registered keep their links.
		$result = new EventLinkCollection( $diff->orphan_keep() );

		// Updates.
		foreach ( $diff->to_update() as $entry ) {

			$link       = $entry['link'];
			$connection = $entry['connection'];
			$slug       = $link->provider_slug();

			$provider = $this->registry->find( CalendarEventProviderInterface::class, $slug );

			if ( ! $provider instanceof CalendarEventProviderInterface ) {
				$result = $result->with( $link );

				continue;
			}

			$response = $provider->update_event( $appointment, $connection, $link->event_id(), $link->to_array() );

			if ( is_wp_error( $response ) ) {
				$errors[ $slug . ':' . $connection->id ] = $response;
				$result                                  = $result->with( $link );

				continue;
			}

			$updated = is_array( $response ) ? ( EventLink::from_array( $response ) ?? $link ) : $link;
			$result  = $result->with( $updated );
		}

		// Deletes.
		foreach ( $diff->to_delete() as $entry ) {

			$link       = $entry['link'];
			$connection = $entry['connection'];
			$slug       = $link->provider_slug();

			$provider = $this->registry->find( CalendarEventProviderInterface::class, $slug );

			if ( ! $provider instanceof CalendarEventProviderInterface ) {
				continue;
			}

			$response = $provider->delete_event( $appointment, $connection, $link->event_id(), $link->to_array() );

			if ( is_wp_error( $response ) ) {
				$errors[ $slug . ':' . $connection->id ] = $response;
			}
		}

		// Pushes.
		foreach ( $diff->to_push() as $entry ) {

			$slug       = $entry['slug'];
			$connection = $entry['connection'];

			$provider = $this->registry->find( CalendarEventProviderInterface::class, $slug );

			if ( ! $provider instanceof CalendarEventProviderInterface ) {
				continue;
			}

			$response = $provider->push_event( $appointment, $connection );

			if ( is_wp_error( $response ) ) {
				$errors[ $slug . ':' . $connection->id ] = $response;

				continue;
			}

			if ( ! is_array( $response ) ) {
				continue;
			}

			$link = EventLink::from_array( $response );

			if ( $link === null ) {
				continue;
			}

			$result = $result->with( $link );
		}

		return new DispatchResult( $result, $errors );
	}

	/**
	 * Cancel every linked event for the given appointment.
	 *
	 * Only dispatches to providers that implement
	 * CancellableCalendarEventProviderInterface. Providers that do not
	 * support cancel are silently skipped — the event stays in their calendar
	 * unchanged. Best-effort: errors are collected but never abort the loop.
	 *
	 * The remote event is NOT deleted. Deletion happens later via
	 * dispatch_delete_all() when the appointment is hard-deleted.
	 *
	 * @since 1.5.0
	 *
	 * @param AppointmentModel    $appointment Target appointment.
	 * @param EventLinkCollection $existing    Links to cancel.
	 *
	 * @return DispatchResult Unchanged new_links collection + accumulated errors.
	 */
	public function dispatch_cancel_all(
		AppointmentModel $appointment,
		EventLinkCollection $existing
	): DispatchResult {

		$errors = [];

		$connections_by_id = OAuthConnectionModel::get_many( $existing->connection_ids() );

		foreach ( $existing->all() as $link ) {

			$slug     = $link->provider_slug();
			$conn_id  = $link->connection_id();
			$provider = $this->registry->find( CalendarEventProviderInterface::class, $slug );

			if ( ! $provider instanceof CancellableCalendarEventProviderInterface ) {
				continue;
			}

			$connection = $connections_by_id[ $conn_id ] ?? null;

			if ( ! $connection instanceof OAuthConnectionModel ) {
				continue;
			}

			$response = $provider->cancel_event( $appointment, $connection, $link->event_id(), $link->to_array() );

			if ( is_wp_error( $response ) ) {
				$errors[ $slug . ':' . $conn_id ] = $response;
			}
		}

		return new DispatchResult( $existing, $errors );
	}

	/**
	 * Delete every linked event for the given appointment.
	 *
	 * Called on hard-delete (appointment_deleted). Best-effort: errors are
	 * collected but never abort the loop.
	 *
	 * @since 1.5.0
	 *
	 * @param AppointmentModel    $appointment Target appointment.
	 * @param EventLinkCollection $existing    Links to permanently delete.
	 *
	 * @return DispatchResult Empty new_links collection + accumulated errors.
	 */
	public function dispatch_delete_all(
		AppointmentModel $appointment,
		EventLinkCollection $existing
	): DispatchResult {

		$errors = [];

		$connections_by_id = OAuthConnectionModel::get_many( $existing->connection_ids() );

		foreach ( $existing->all() as $link ) {

			$slug     = $link->provider_slug();
			$conn_id  = $link->connection_id();
			$provider = $this->registry->find( CalendarEventProviderInterface::class, $slug );

			if ( ! $provider instanceof CalendarEventProviderInterface ) {
				continue;
			}

			$connection = $connections_by_id[ $conn_id ] ?? null;

			if ( ! $connection instanceof OAuthConnectionModel ) {
				continue;
			}

			$response = $provider->delete_event( $appointment, $connection, $link->event_id(), $link->to_array() );

			if ( is_wp_error( $response ) ) {
				$errors[ $slug . ':' . $conn_id ] = $response;
			}
		}

		return new DispatchResult( EventLinkCollection::empty(), $errors );
	}
}
