<?php

declare(strict_types=1);

namespace Drupal\flowdrop_interrupt\EventSubscriber;

use Drupal\flowdrop_interrupt\Event\InterruptResolvedEvent;
use Drupal\flowdrop_interrupt\Service\InterruptManagerInterface;
use Psr\Log\LoggerInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;

/**
 * Cascades resolution to interrupts linked to the just-resolved one.
 *
 * When an interrupt resolves, any pending interrupt whose
 * `linked_interrupt_id` equals the resolved UUID is resolved with the same
 * response_data. This is the generic primitive that supersedes per-type
 * cascade subscribers — anything that needs "resolve me when X resolves"
 * sets `linked_interrupt_id = X.uuid` at creation time and gets cascaded
 * for free.
 *
 * Runs at high priority so cascade resolutions happen before the
 * pipeline-resume subscriber acts on the originating event — followers'
 * own resolutions will fire their own InterruptResolvedEvents and trigger
 * resume independently.
 *
 * @internal
 */
final class LinkedInterruptResolvedSubscriber implements EventSubscriberInterface {

  /**
   * Maximum cascade depth before we suspect a cycle and bail.
   *
   * Real workflows nest sub-workflows a handful of levels deep at most; 16
   * is well above plausible use and below recursion limits.
   */
  private const MAX_CASCADE_DEPTH = 16;

  /**
   * UUIDs currently being cascaded through, in this request.
   *
   * Used to detect A→B→A (or longer) link cycles. Keyed by UUID, value is
   * always TRUE — the set semantics matter, not the value.
   *
   * @var array<string, true>
   */
  private array $inFlight = [];

  public function __construct(
    private readonly InterruptManagerInterface $interruptManager,
    private readonly LoggerInterface $logger,
  ) {}

  /**
   * {@inheritdoc}
   */
  public static function getSubscribedEvents(): array {
    // Priority 100: ahead of InterruptResolvedSubscriber (-10) and
    // SessionInterruptResolvedSubscriber (default 0) so followers resolve
    // before the resumer acts on the original.
    return [
      InterruptResolvedEvent::NAME => ['onInterruptResolved', 100],
    ];
  }

  /**
   * Resolves any pending followers with the just-resolved interrupt's payload.
   *
   * The follower's own resolution fires another InterruptResolvedEvent, which
   * re-enters this method. The {@see self::$inFlight} set + depth cap detect
   * transitive cycles (A→B→A, A→B→C→A, …) and abort with a logged error
   * before stack overflow or lock contention.
   */
  public function onInterruptResolved(InterruptResolvedEvent $event): void {
    $resolvedUuid = (string) ($event->interrupt->uuid() ?? '');
    if ($resolvedUuid === '') {
      return;
    }

    // Cycle guard: this UUID is already being cascaded through earlier in
    // the call chain. Logging once at the cycle entry point is enough.
    if (isset($this->inFlight[$resolvedUuid])) {
      $this->logger->error(
        'Linked-interrupt cycle detected at @uuid (chain: @chain); aborting cascade.',
        [
          '@uuid' => $resolvedUuid,
          '@chain' => implode(' → ', array_keys($this->inFlight)),
        ],
      );
      return;
    }
    if (count($this->inFlight) >= self::MAX_CASCADE_DEPTH) {
      $this->logger->error(
        'Linked-interrupt cascade exceeded depth @max at @uuid (chain: @chain); aborting.',
        [
          '@max' => self::MAX_CASCADE_DEPTH,
          '@uuid' => $resolvedUuid,
          '@chain' => implode(' → ', array_keys($this->inFlight)),
        ],
      );
      return;
    }

    $followers = $this->interruptManager->getPendingInterruptsLinkedTo($resolvedUuid);
    if ($followers === []) {
      return;
    }

    $this->inFlight[$resolvedUuid] = TRUE;
    try {
      foreach ($followers as $follower) {
        $followerUuid = (string) ($follower->uuid() ?? '');
        if ($followerUuid === '' || $followerUuid === $resolvedUuid) {
          // Self-link guard — silently skip.
          continue;
        }
        if (isset($this->inFlight[$followerUuid])) {
          // Follower is already mid-cascade — direct cycle.
          $this->logger->error(
            'Linked-interrupt cycle detected: @follower already in-flight (chain: @chain); skipping.',
            [
              '@follower' => $followerUuid,
              '@chain' => implode(' → ', array_keys($this->inFlight)),
            ],
          );
          continue;
        }
        try {
          $this->interruptManager->resolveInterrupt(
            $followerUuid,
            $event->responseData,
            $event->resolvedBy,
          );
        }
        catch (\Throwable $e) {
          $this->logger->error('Cascade resolution failed for interrupt @follower linked to @resolved: @message', [
            '@follower' => $followerUuid,
            '@resolved' => $resolvedUuid,
            '@message' => $e->getMessage(),
          ]);
        }
      }
    }
    finally {
      unset($this->inFlight[$resolvedUuid]);
    }
  }

}
