<?php

namespace SugarCalendarBookings\Integrations\CalendarEvents;

/**
 * Immutable typed collection of calendar event links.
 *
 * Every mutation returns a new collection. Replaces the loose
 * `array<string, mixed>` lists previously used in CalendarEventManager.
 *
 * @since 1.5.0
 */
class EventLinkCollection {

	/**
	 * Links contained in the collection.
	 *
	 * @since 1.5.0
	 *
	 * @var EventLink[]
	 */
	private array $links;

	/**
	 * Constructor.
	 *
	 * @since 1.5.0
	 *
	 * @param EventLink[] $links Links to wrap.
	 */
	public function __construct( array $links = [] ) {

		$this->links = array_values(
			array_filter(
				$links,
				static fn( $link ) => $link instanceof EventLink
			)
		);
	}

	/**
	 * Build an empty collection.
	 *
	 * @since 1.5.0
	 *
	 * @return self
	 */
	public static function empty(): self {

		return new self();
	}

	/**
	 * Build a collection from raw stored rows, tolerating malformed entries.
	 *
	 * @since 1.5.0
	 *
	 * @param array $rows Raw rows from storage.
	 *
	 * @return self
	 */
	public static function from_raw( array $rows ): self {

		$links = [];

		foreach ( $rows as $row ) {

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

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

			if ( $link !== null ) {
				$links[] = $link;
			}
		}

		return new self( $links );
	}

	/**
	 * Check whether a link exists for the given provider and connection.
	 *
	 * @since 1.5.0
	 *
	 * @param string $provider_slug Provider slug.
	 * @param int    $connection_id Connection ID.
	 *
	 * @return bool
	 */
	public function has( string $provider_slug, int $connection_id ): bool {

		return $this->find( $provider_slug, $connection_id ) !== null;
	}

	/**
	 * Find the link matching the given provider and connection.
	 *
	 * @since 1.5.0
	 *
	 * @param string $provider_slug Provider slug.
	 * @param int    $connection_id Connection ID.
	 *
	 * @return EventLink|null
	 */
	public function find( string $provider_slug, int $connection_id ): ?EventLink {

		foreach ( $this->links as $link ) {

			if ( $link->matches( $provider_slug, $connection_id ) ) {
				return $link;
			}
		}

		return null;
	}

	/**
	 * Return a new collection with the given link added or replaced.
	 *
	 * @since 1.5.0
	 *
	 * @param EventLink $link Link to add.
	 *
	 * @return self
	 */
	public function with( EventLink $link ): self {

		$next = $this->without( $link->provider_slug(), $link->connection_id() )->links;

		$next[] = $link;

		return new self( $next );
	}

	/**
	 * Return a new collection with the matching link removed.
	 *
	 * @since 1.5.0
	 *
	 * @param string $provider_slug Provider slug.
	 * @param int    $connection_id Connection ID.
	 *
	 * @return self
	 */
	public function without( string $provider_slug, int $connection_id ): self {

		$next = [];

		foreach ( $this->links as $link ) {

			if ( $link->matches( $provider_slug, $connection_id ) ) {
				continue;
			}

			$next[] = $link;
		}

		return new self( $next );
	}

	/**
	 * Whether the collection has no links.
	 *
	 * @since 1.5.0
	 *
	 * @return bool
	 */
	public function is_empty(): bool {

		return $this->links === [];
	}

	/**
	 * Get all links in the collection.
	 *
	 * @since 1.5.0
	 *
	 * @return EventLink[]
	 */
	public function all(): array {

		return $this->links;
	}

	/**
	 * Get the unique connection IDs referenced by every link in the collection.
	 *
	 * Used by callers that need to batch-resolve the underlying connections in
	 * a single DB query rather than once per link.
	 *
	 * @since 1.5.0
	 *
	 * @return int[]
	 */
	public function connection_ids(): array {

		$ids = array_map(
			static fn( EventLink $link ) => $link->connection_id(),
			$this->links
		);

		return array_values( array_unique( $ids ) );
	}

	/**
	 * Serialize all links to their canonical storage shape.
	 *
	 * @since 1.5.0
	 *
	 * @return array<int, array<string, mixed>>
	 */
	public function to_array(): array {

		return array_map(
			static fn( EventLink $link ) => $link->to_array(),
			$this->links
		);
	}
}
