<?php

declare(strict_types=1);

namespace Drupal\flowdrop_runtime\Event;

use Symfony\Contracts\EventDispatcher\Event;

/**
 * Polls for an inward Cancel/Pause signal targeting a running pipeline.
 *
 * Bridges the dependency-direction split between `flowdrop_runtime` and
 * `flowdrop_interrupt` (same pattern as JobInterruptCaughtEvent).
 * `flowdrop_runtime` dispatches this event between job iterations;
 * `flowdrop_interrupt` subscribes, runs the indexed lookup, and attaches
 * any pending signal back to the event via {@see self::attachCancel()} or
 * {@see self::attachPause()}.
 *
 * The orchestrator then acts on the attached signal: cancelling or
 * pausing the pipeline as appropriate. Subscribers MUST NOT throw — the
 * orchestrator's poll path expects to continue normally if no subscriber
 * is installed.
 *
 * @internal
 */
class PipelineSignalPollEvent extends Event {

  /**
   * The event name.
   */
  const NAME = 'flowdrop_runtime.pipeline.poll_signal';

  /**
   * Action constants attached by subscribers.
   */
  const ACTION_CANCEL = 'cancel';
  const ACTION_PAUSE = 'pause';

  /**
   * The attached signal, if any.
   *
   * @var array{action: string, signal_id: string, reason: ?string, initiator_uid: ?int}|null
   */
  private ?array $signal = NULL;

  /**
   * Constructs a new PipelineSignalPollEvent.
   *
   * @param string $pipelineId
   *   The pipeline being polled.
   * @param string $executionId
   *   The current execution ID.
   * @param string $workflowId
   *   The workflow ID.
   */
  public function __construct(
    public readonly string $pipelineId,
    public readonly string $executionId,
    public readonly string $workflowId,
  ) {}

  /**
   * Attaches a Cancel signal. Called by the subscriber after acknowledging.
   */
  public function attachCancel(string $signalId, ?string $reason, ?int $initiatorUid): void {
    $this->setSignal(self::ACTION_CANCEL, $signalId, $reason, $initiatorUid);
  }

  /**
   * Attaches a Pause signal.
   *
   * The signal stays pending — resolving it later (via the inbox or
   * `/resume`) is what unpauses the pipeline.
   */
  public function attachPause(string $signalId, ?string $reason, ?int $initiatorUid): void {
    $this->setSignal(self::ACTION_PAUSE, $signalId, $reason, $initiatorUid);
  }

  /**
   * Returns the attached signal payload, or NULL if none attached.
   *
   * @return array{action: string, signal_id: string, reason: ?string, initiator_uid: ?int}|null
   *   The signal payload.
   */
  public function getSignal(): ?array {
    return $this->signal;
  }

  /**
   * Sets the attached signal payload (single-attachment enforcement).
   *
   * @internal Used by attachCancel/attachPause to enforce single-attachment.
   */
  private function setSignal(string $action, string $signalId, ?string $reason, ?int $initiatorUid): void {
    if ($this->signal !== NULL) {
      throw new \LogicException(
        'PipelineSignalPollEvent: a signal is already attached. Subscribers '
        . 'must not race each other.'
      );
    }
    $this->signal = [
      'action' => $action,
      'signal_id' => $signalId,
      'reason' => $reason,
      'initiator_uid' => $initiatorUid,
    ];
  }

}
