<?php

declare(strict_types=1);

namespace Drupal\flowdrop_runtime\Service\Runtime;

use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\flowdrop\DTO\ParameterBag;
use Drupal\flowdrop\Plugin\FlowDropNodeProcessor\CancellableNodeProcessorInterface;
use Drupal\flowdrop\Service\FlowDropNodeProcessorPluginManager;
use Drupal\flowdrop_job\FlowDropJobInterface;
use Drupal\flowdrop_node_type\FlowDropNodeTypeInterface;
use Drupal\flowdrop_orchestration\DTO\ResolvedInterruptInterface;
use Psr\Log\LoggerInterface;

/**
 * Invokes cancel() on node processors when outward interrupts auto-cancel.
 *
 * Triggered when an inward Cancel signal auto-cancels outward interrupts.
 *
 * The interrupt module's PipelineSignalPollSubscriber calls this once per
 * pending outward interrupt before marking the entity cancelled. The
 * dispatcher resolves the node processor from the interrupt's job_id +
 * node_type_id, instantiates it, and calls cancel() only if the processor
 * implements {@see CancellableNodeProcessorInterface}.
 *
 * Errors are logged, never thrown — teardown is best-effort. The pipeline
 * is already being cancelled; a failing cleanup must not block that.
 *
 * @internal
 */
class InterruptCancellationDispatcher {

  public function __construct(
    private readonly EntityTypeManagerInterface $entityTypeManager,
    private readonly FlowDropNodeProcessorPluginManager $nodeProcessorManager,
    private readonly LoggerInterface $logger,
  ) {}

  /**
   * Invokes the cancel() hook for the node behind an interrupt, if any.
   *
   * Silently no-ops when:
   *  - `jobId` is NULL (legacy row without job_id stamped).
   *  - The job doesn't exist (stale reference).
   *  - The job's metadata doesn't carry `node_type_id`.
   *  - The node type entity is missing.
   *  - The processor doesn't implement CancellableNodeProcessorInterface.
   *
   * @param int|null $jobId
   *   The job entity ID stamped on the interrupt by the orchestrator.
   * @param \Drupal\flowdrop_orchestration\DTO\ResolvedInterruptInterface $interrupt
   *   The interrupt entity being cancelled.
   */
  public function invokeCancelHook(?int $jobId, ResolvedInterruptInterface $interrupt): void {
    if ($jobId === NULL) {
      return;
    }
    try {
      $job = $this->entityTypeManager->getStorage('flowdrop_job')->load($jobId);
      if (!$job instanceof FlowDropJobInterface) {
        return;
      }
      $nodeTypeId = $job->getMetadataValue('node_type_id', NULL);
      if (!is_string($nodeTypeId) || $nodeTypeId === '') {
        return;
      }
      $nodeTypeEntity = $this->entityTypeManager
        ->getStorage('flowdrop_node_type')
        ->load($nodeTypeId);
      if (!$nodeTypeEntity instanceof FlowDropNodeTypeInterface) {
        return;
      }
      $processor = $this->nodeProcessorManager->createInstance($nodeTypeEntity->getExecutorPlugin());
      if (!$processor instanceof CancellableNodeProcessorInterface) {
        return;
      }
      $params = new ParameterBag($job->getInputData() ?: []);
      $processor->cancel($interrupt, $params);
    }
    catch (\Throwable $e) {
      // Best-effort: log and continue. The orchestrator will still mark
      // the interrupt cancelled and the pipeline will still tear down.
      $this->logger->warning(
        'cancel() hook failed for interrupt @id (job @job): @msg',
        [
          '@id' => $interrupt->getInterruptId(),
          '@job' => $jobId,
          '@msg' => $e->getMessage(),
        ],
      );
    }
  }

}
