<?php

declare(strict_types=1);

namespace Drupal\flowdrop_interrupt\EventSubscriber;

use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\flowdrop_interrupt\DTO\ExecutionStamp;
use Drupal\flowdrop_interrupt\Entity\FlowDropInterruptInterface;
use Drupal\flowdrop_interrupt\Exception\InterruptRequiredException;
use Drupal\flowdrop_interrupt\Service\InterruptMessageBusInterface;
use Drupal\flowdrop_job\FlowDropJobInterface;
use Drupal\flowdrop_runtime\Event\JobInterruptCaughtEvent;
use Psr\Log\LoggerInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;

/**
 * Bridges the runtime's catch site to the persists.
 *
 * The runtime (flowdrop_runtime) deliberately does not depend on the
 * interrupt module (flowdrop_interrupt). When NodeRuntimeService
 * catches an InterruptExceptionInterface it dispatches
 * JobInterruptCaughtEvent; this subscriber handles the
 * two interrupt-module-specific concerns:
 *
 *  1. **Message-shape persistence.** If the exception carries an
 *     InterruptMessageInterface DTO, this
 *     subscriber constructs an {@see ExecutionStamp}
 *     from event data and
 *     dispatches via the {@see InterruptMessageBusInterface}.
 *     The bus's per-shape handler persists the entity. The
 *     subscriber then marks the job interrupted and attaches
 *     the entity to the event via
 *     {@see JobInterruptCaughtEvent::setInterrupt()}.
 *
 *  2. **Field stamping on pre-persisted entities.** Existing
 *     interrupts may be missing `job_id`. This subscriber stamps
 *     it when missing. The pre-persisted entity is also
 *     attached to the event for a uniform read
 *     path downstream.
 *
 * The exception itself stays immutable — the event is the mutation point.
 *
 * @internal
 */
final class JobInterruptCaughtSubscriber implements EventSubscriberInterface {

  public function __construct(
    private readonly InterruptMessageBusInterface $messageBus,
    private readonly EntityTypeManagerInterface $entityTypeManager,
    private readonly LoggerInterface $logger,
  ) {}

  /**
   * {@inheritdoc}
   */
  public static function getSubscribedEvents(): array {
    return [
      JobInterruptCaughtEvent::NAME => ['onInterruptCaught', 0],
    ];
  }

  /**
   * Persists message-shape exceptions and stamps fields on entity-shape ones.
   *
   * Failures throw — the runtime needs to know that a message-shape exception
   * couldn't be persisted, otherwise downstream code would fail later with
   * confusing errors about a missing entity.
   */
  public function onInterruptCaught(JobInterruptCaughtEvent $event): void {
    $exception = $event->exception;
    if (!$exception instanceof InterruptRequiredException) {
      return;
    }

    $message = $exception->getInterruptMessage();
    $existingEntity = $exception->getInterrupt();

    // Message shape: dispatch via the bus, mark the job, attach to event.
    if ($message !== NULL) {
      $entity = $this->messageBus->dispatch($message, new ExecutionStamp(
        workflowId: $event->workflowId,
        nodeId: $event->nodeId,
        pipelineId: $event->pipelineId,
        sessionId: $event->sessionId,
        jobId: $event->jobId,
      ));
      $this->markJobInterrupted($event->jobId, $entity, $exception->getMessage());
      $event->setInterrupt($entity);
      return;
    }

    // Entity shape: stamp job_id if missing, then attach.
    if ($existingEntity !== NULL) {
      $this->stampJobId($existingEntity, $event->jobId);
      $event->setInterrupt($existingEntity);
    }
  }

  /**
   * Stamps job_id on a pre-persisted entity if missing.
   */
  private function stampJobId(
    FlowDropInterruptInterface $entity,
    ?int $jobId,
  ): void {
    if ($jobId === NULL || $entity->getJobId() !== NULL) {
      return;
    }
    $entity->setJobId($jobId);
    $entity->save();
  }

  /**
   * Loads the job and marks it as interrupted.
   *
   * For message-shape exceptions this replaces the orchestrator's own
   * markAsInterrupted call — the orchestrator detects the already-marked
   * status and skips.
   */
  private function markJobInterrupted(
    ?int $jobId,
    FlowDropInterruptInterface $entity,
    string $message,
  ): void {
    if ($jobId === NULL) {
      // Stateless orchestrator — no job to mark. The message-shape entity
      // still exists; the orchestrator may not be able to resume it, but
      // that's a separate concern logged elsewhere.
      $this->logger->debug(
        'Message-shape interrupt @uuid persisted with no job to mark; '
        . 'stateless orchestrator path.',
        ['@uuid' => $entity->uuid()],
      );
      return;
    }
    $job = $this->entityTypeManager->getStorage('flowdrop_job')->load($jobId);
    if (!$job instanceof FlowDropJobInterface) {
      throw new \RuntimeException(sprintf(
        'JobInterruptCaughtSubscriber: job %d not found while marking '
        . 'interrupt %s; the runtime had a stale or invalid job_id.',
        $jobId,
        $entity->uuid() ?? '(no uuid)',
      ));
    }
    $job->markAsInterrupted((string) ($entity->uuid() ?? ''), $message);
    $this->entityTypeManager->getStorage('flowdrop_job')->save($job);
  }

}
