<?php

declare(strict_types=1);

namespace Drupal\flowdrop_runtime\Event;

use Drupal\flowdrop_interrupt\Entity\FlowDropInterruptInterface;
use Drupal\flowdrop_orchestration\Exception\InterruptExceptionInterface;
use Symfony\Contracts\EventDispatcher\Event;

/**
 * Dispatched right after an interrupt exception is caught at the node layer.
 *
 * Bridges the dependency-direction split between `flowdrop_runtime` (which
 * does NOT depend on `flowdrop_interrupt`) and `flowdrop_interrupt` (which
 * does depend on the runtime).
 *
 * The event is the **mutation point** for the descriptor-shape flow:
 *
 * - The exception itself stays immutable.
 * - Subscribers may persist a descriptor-shape exception's request DTO,
 *   call `markAsInterrupted` directly on the job, then attach the
 *   resulting entity to this event via {@see self::setInterrupt()}.
 * - The runtime catch site reads the persisted entity from this event
 *   (not from the exception) when updating real-time status.
 *
 * For legacy entity-bearing exceptions the entity is available directly
 * on the exception; the event is still dispatched so subscribers can stamp
 * orchestrator-controlled fields (direction, job_id) if missing.
 *
 * @internal
 */
class JobInterruptCaughtEvent extends Event {

  /**
   * The event name.
   */
  const NAME = 'flowdrop_runtime.job.interrupt_caught';

  /**
   * The persisted entity, available after a subscriber has stored it.
   */
  private ?FlowDropInterruptInterface $interrupt = NULL;

  /**
   * Constructs a new JobInterruptCaughtEvent.
   *
   * @param \Drupal\flowdrop_orchestration\Exception\InterruptExceptionInterface $exception
   *   The caught exception. Read-only.
   * @param string $workflowId
   *   The workflow being executed.
   * @param string|null $pipelineId
   *   The pipeline being executed, if pipeline-based.
   * @param string $nodeId
   *   The node that threw.
   * @param int|null $jobId
   *   The job entity ID for pipeline-based orchestrators; NULL for
   *   stateless ones.
   * @param int|null $sessionId
   *   The playground session ID, if running under a session.
   */
  public function __construct(
    public readonly InterruptExceptionInterface $exception,
    public readonly string $workflowId,
    public readonly ?string $pipelineId,
    public readonly string $nodeId,
    public readonly ?int $jobId,
    public readonly ?int $sessionId,
  ) {}

  /**
   * Returns the persisted interrupt entity, if a subscriber has attached one.
   *
   * For legacy-shape exceptions a subscriber should attach the entity from
   * the exception so consumers have a uniform read path. For descriptor-
   * shape exceptions the subscriber attaches the freshly-persisted entity
   * after creating it.
   */
  public function getInterrupt(): ?FlowDropInterruptInterface {
    return $this->interrupt;
  }

  /**
   * Attaches the persisted entity. Called once by the persistence subscriber.
   *
   * Throws if the entity has already been attached — subscribers must not
   * race each other. The runtime's catch site reads via {@see getInterrupt()}.
   */
  public function setInterrupt(FlowDropInterruptInterface $interrupt): void {
    if ($this->interrupt !== NULL) {
      throw new \LogicException(
        'JobInterruptCaughtEvent::setInterrupt() may be called once. '
        . 'Another subscriber already attached an interrupt to this event.'
      );
    }
    $this->interrupt = $interrupt;
  }

}
