<?php

namespace SugarCalendarBookings\Integrations\CalendarEvents;

/**
 * Calendar event link value object.
 *
 * Represents a single mirror between a Sugar Calendar appointment and an
 * external calendar event, identified by (provider, connection_id, event_id).
 *
 * Immutable: properties are private, no setters. Reconstruct to mutate.
 *
 * @since 1.5.0
 */
class EventLink {

	/**
	 * Provider slug (e.g. "outlook", "google_calendar").
	 *
	 * @since 1.5.0
	 *
	 * @var string
	 */
	private string $provider_slug;

	/**
	 * OAuth connection ID this event belongs to.
	 *
	 * @since 1.5.0
	 *
	 * @var int
	 */
	private int $connection_id;

	/**
	 * Provider-side event ID.
	 *
	 * @since 1.5.0
	 *
	 * @var string
	 */
	private string $event_id;

	/**
	 * Provider-specific extra metadata (e.g. calendar_id).
	 *
	 * @since 1.5.0
	 *
	 * @var array<string, mixed>
	 */
	private array $extra;

	/**
	 * Constructor.
	 *
	 * @since 1.5.0
	 *
	 * @param string               $provider_slug Provider slug.
	 * @param int                  $connection_id OAuth connection ID.
	 * @param string               $event_id      Provider event ID.
	 * @param array<string, mixed> $extra         Provider-specific extra metadata.
	 */
	public function __construct(
		string $provider_slug,
		int $connection_id,
		string $event_id,
		array $extra = []
	) {

		$this->provider_slug = $provider_slug;
		$this->connection_id = $connection_id;
		$this->event_id      = $event_id;
		$this->extra         = $extra;
	}

	/**
	 * Build an EventLink from a raw associative array.
	 *
	 * Returns null when required keys are missing or malformed. Tolerates
	 * legacy shapes that nested extra data alongside the canonical fields.
	 *
	 * @since 1.5.0
	 *
	 * @param array<string, mixed> $raw Raw stored link.
	 *
	 * @return self|null
	 */
	public static function from_array( array $raw ): ?self {

		$provider_slug = isset( $raw['provider'] ) ? (string) $raw['provider'] : '';
		$connection_id = isset( $raw['connection_id'] ) ? (int) $raw['connection_id'] : 0;
		$event_id      = isset( $raw['event_id'] ) ? (string) $raw['event_id'] : '';

		if ( $provider_slug === '' || $connection_id <= 0 || $event_id === '' ) {
			return null;
		}

		$extra = isset( $raw['extra'] ) && is_array( $raw['extra'] ) ? $raw['extra'] : [];

		// Tolerate flat shapes: providers return provider-specific fields
		// (calendar_id, event_url, pushed_at, ...) at the top level. Promote
		// any non-canonical top-level key into extra so it round-trips through
		// storage instead of being silently dropped.
		foreach ( $raw as $key => $value ) {

			if ( in_array( $key, [ 'provider', 'connection_id', 'event_id', 'extra' ], true ) ) {
				continue;
			}

			if ( ! array_key_exists( $key, $extra ) ) {
				$extra[ $key ] = $value;
			}
		}

		return new self( $provider_slug, $connection_id, $event_id, $extra );
	}

	/**
	 * Get the provider slug.
	 *
	 * @since 1.5.0
	 *
	 * @return string
	 */
	public function provider_slug(): string {

		return $this->provider_slug;
	}

	/**
	 * Get the OAuth connection ID.
	 *
	 * @since 1.5.0
	 *
	 * @return int
	 */
	public function connection_id(): int {

		return $this->connection_id;
	}

	/**
	 * Get the provider event ID.
	 *
	 * @since 1.5.0
	 *
	 * @return string
	 */
	public function event_id(): string {

		return $this->event_id;
	}

	/**
	 * Check whether this link targets the given provider and connection.
	 *
	 * @since 1.5.0
	 *
	 * @param string $provider_slug Provider slug to match.
	 * @param int    $connection_id Connection ID to match.
	 *
	 * @return bool
	 */
	public function matches( string $provider_slug, int $connection_id ): bool {

		return $this->provider_slug === $provider_slug
			&& $this->connection_id === $connection_id;
	}

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

		return [
			'provider'      => $this->provider_slug,
			'connection_id' => $this->connection_id,
			'event_id'      => $this->event_id,
			'extra'         => $this->extra,
		];
	}
}
