<?php

declare( strict_types = 1 );

namespace Automattic\WooCommerce\Internal\PushNotifications\Services;

defined( 'ABSPATH' ) || exit;

use Automattic\WooCommerce\Internal\PushNotifications\Dispatchers\InternalNotificationDispatcher;
use Automattic\WooCommerce\Internal\PushNotifications\Notifications\Notification;
use Automattic\WooCommerce\Internal\PushNotifications\Services\NotificationProcessor;

/**
 * Store that collects notifications during a request and dispatches them all on
 * shutdown via the InternalNotificationDispatcher. Should be accessed from the
 * container (`wc_get_container`) to ensure store is shared by all usage.
 *
 * Notifications are keyed by `{type}_{resource_id}` (with blog ID from
 * `get_current_blog_id()`) to prevent duplicates within a single request.
 *
 * @since 10.7.0
 */
class PendingNotificationStore {
	/**
	 * Whether the store is enabled and accepting notifications.
	 *
	 * @var bool
	 */
	private bool $enabled = false;

	/**
	 * The dispatcher that will be used to send notifications on shutdown.
	 *
	 * @var InternalNotificationDispatcher
	 */
	private InternalNotificationDispatcher $dispatcher;

	/**
	 * Pending notifications keyed by identifier.
	 *
	 * @var array<string, Notification>
	 */
	private array $pending = array();

	/**
	 * Whether the shutdown hook has been registered.
	 *
	 * @var bool
	 */
	private bool $shutdown_registered = false;

	/**
	 * Initialize dependencies.
	 *
	 * @internal
	 *
	 * @param InternalNotificationDispatcher $dispatcher The dispatcher to use on shutdown.
	 *
	 * @since 10.7.0
	 */
	final public function init( InternalNotificationDispatcher $dispatcher ): void {
		$this->dispatcher = $dispatcher;
	}

	/**
	 * Enables the store so it accepts notifications.
	 *
	 * Called from PushNotifications::on_init() after enablement checks pass.
	 *
	 * @return void
	 *
	 * @since 10.7.0
	 */
	public function register(): void {
		$this->enabled = true;
	}

	/**
	 * Adds a notification to the pending store.
	 *
	 * Duplicate notifications (same type and resource ID) within a single
	 * request are silently ignored. The shutdown hook is registered on the
	 * first call.
	 *
	 * @param Notification $notification The notification to add.
	 * @return void
	 *
	 * @since 10.7.0
	 */
	public function add( Notification $notification ): void {
		if ( ! $this->enabled ) {
			return;
		}

		$key = $notification->get_identifier();

		if ( isset( $this->pending[ $key ] ) ) {
			return;
		}

		$this->pending[ $key ] = $notification;

		$this->schedule_safety_net( $notification );

		if ( ! $this->shutdown_registered ) {
			add_action( 'shutdown', array( $this, 'dispatch_all' ) );
			$this->shutdown_registered = true;
		}
	}

	/**
	 * Schedules an ActionScheduler safety net job for the notification.
	 *
	 * If the shutdown hook never fires (OOM, SIGKILL, etc.), this job
	 * guarantees the notification is still processed.
	 *
	 * @param Notification $notification The notification to schedule.
	 * @return void
	 *
	 * @since 10.7.0
	 */
	private function schedule_safety_net( Notification $notification ): void {
		// Canonical, identity-keyed args shared with NotificationProcessor::cancel_safety_net().
		// Action Scheduler matches stored args by exact equality, so both sides must derive
		// them from the same place; see Notification::get_safety_net_args().
		$args = $notification->get_safety_net_args();

		if ( as_has_scheduled_action( NotificationProcessor::SAFETY_NET_HOOK, $args, NotificationProcessor::ACTION_SCHEDULER_GROUP ) ) {
			return;
		}

		as_schedule_single_action(
			time() + NotificationProcessor::SAFETY_NET_DELAY,
			NotificationProcessor::SAFETY_NET_HOOK,
			$args,
			NotificationProcessor::ACTION_SCHEDULER_GROUP,
			true
		);
	}

	/**
	 * Dispatches all pending notifications via InternalNotificationDispatcher.
	 *
	 * Called on shutdown. Sends all pending notifications through the
	 * InternalNotificationDispatcher, then clears the store.
	 *
	 * @return void
	 *
	 * @since 10.7.0
	 */
	public function dispatch_all(): void {
		if ( empty( $this->pending ) ) {
			return;
		}

		$this->dispatcher->dispatch( array_values( $this->pending ) );

		$this->enabled = false;
		$this->pending = array();
	}

	/**
	 * Returns the number of pending notifications.
	 *
	 * @return int
	 *
	 * @since 10.7.0
	 */
	public function count(): int {
		return count( $this->pending );
	}

	/**
	 * Returns all pending notifications.
	 *
	 * @return Notification[]
	 *
	 * @since 10.7.0
	 */
	public function get_all(): array {
		return array_values( $this->pending );
	}
}
