<?php

declare(strict_types=1);

namespace Drupal\flowdrop_interrupt\EventSubscriber;

use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\flowdrop_interrupt\Entity\FlowDropInterruptInterface;
use Drupal\flowdrop_interrupt\Event\InterruptResolvedEvent;
use Drupal\flowdrop_job\FlowDropJobInterface;
use Drupal\flowdrop_pipeline\Entity\FlowDropPipelineInterface;
use Drupal\flowdrop_runtime\Service\Orchestrator\PipelineExecutorInterface;
use Psr\Log\LoggerInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;

/**
 * Event subscriber for handling resolved interrupts.
 *
 * When an interrupt is resolved (user provides input), this subscriber:
 * 1. Finds the interrupted job by pipeline and node ID.
 * 2. Resets the job status from "interrupted" to "pending".
 * 3. Resumes the pipeline execution.
 *
 * Generic subscriber for any pipeline-based orchestrator.
 * Does NOT handle playground-specific logic (session status, messages).
 *
 * @internal This class is not part of the public API and may change without notice.
 */
class InterruptResolvedSubscriber implements EventSubscriberInterface {

  /**
   * Constructs an InterruptResolvedSubscriber.
   *
   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager
   *   The entity type manager.
   * @param \Drupal\flowdrop_runtime\Service\Orchestrator\PipelineExecutorInterface $pipelineOrchestrator
   *   The synchronous pipeline orchestrator.
   * @param \Psr\Log\LoggerInterface $logger
   *   The logger.
   */
  public function __construct(
    protected readonly EntityTypeManagerInterface $entityTypeManager,
    protected readonly PipelineExecutorInterface $pipelineOrchestrator,
    protected readonly LoggerInterface $logger,
  ) {}

  /**
   * {@inheritdoc}
   */
  public static function getSubscribedEvents(): array {
    // Use a lower priority so playground-specific subscribers run first.
    // If a playground subscriber handles the event and needs special handling,
    // it can stop propagation.
    return [
      "flowdrop_interrupt.resolved" => ["onInterruptResolved", -10],
    ];
  }

  /**
   * Handles the interrupt resolved event.
   *
   * When an interrupt is resolved, this method:
   * 1. Finds the interrupted job by pipeline and node ID.
   * 2. Resets the job status from "interrupted" to "pending".
   * 3. Resumes the pipeline execution.
   *
   * @param \Symfony\Component\EventDispatcher\GenericEvent $event
   *   The event containing the resolved interrupt.
   */
  public function onInterruptResolved(InterruptResolvedEvent $event): void {
    $interrupt = $event->interrupt;

    if (!$interrupt instanceof FlowDropInterruptInterface) {
      return;
    }

    $pipelineId = $interrupt->getPipelineId();
    $nodeId = $interrupt->getNodeId();
    $interruptId = $interrupt->uuid();

    // Skip if no pipeline ID (stateless orchestrator - can't resume).
    if ($pipelineId === NULL) {
      $this->logger->debug("Interrupt @id resolved but has no pipeline ID - skipping resume", [
        "@id" => $interruptId,
      ]);
      return;
    }

    // Load the pipeline.
    $pipeline = $this->loadPipeline($pipelineId);
    if ($pipeline === NULL) {
      $this->logger->warning("Interrupt @id resolved but pipeline @pipeline not found", [
        "@id" => $interruptId,
        "@pipeline" => $pipelineId,
      ]);
      return;
    }

    // Find and reset the interrupted job.
    $job = $this->findAndResetInterruptedJob($pipeline, $nodeId, $interruptId);
    if ($job === NULL) {
      $this->logger->warning("No interrupted job found for node @node in pipeline @pipeline", [
        "@node" => $nodeId,
        "@pipeline" => $pipelineId,
      ]);
      // Continue anyway - the job might have been manually reset or
      // this is a duplicate event.
    }

    // Only resume if pipeline is paused.
    if ($pipeline->getStatus() !== "paused") {
      $this->logger->info("Pipeline @pipeline is not paused (status: @status), skipping resume", [
        "@pipeline" => $pipelineId,
        "@status" => $pipeline->getStatus(),
      ]);
      return;
    }

    // Resume the pipeline execution.
    try {
      $this->logger->info("Resuming pipeline @pipeline after interrupt @interrupt resolved", [
        "@pipeline" => $pipelineId,
        "@interrupt" => $interruptId,
      ]);

      $response = $this->pipelineOrchestrator->executePipeline($pipeline);

      $this->logger->info("Pipeline @pipeline resumed with status: @status", [
        "@pipeline" => $pipelineId,
        "@status" => $response->getStatus(),
      ]);
    }
    catch (\Exception $e) {
      $this->logger->error("Failed to resume pipeline @pipeline: @message", [
        "@pipeline" => $pipelineId,
        "@message" => $e->getMessage(),
      ]);
    }
  }

  /**
   * Loads a pipeline by ID.
   *
   * @param string $pipeline_id
   *   The pipeline ID (entity ID or execution ID).
   *
   * @return \Drupal\flowdrop_pipeline\Entity\FlowDropPipelineInterface|null
   *   The pipeline entity, or NULL if not found.
   */
  protected function loadPipeline(string $pipeline_id): ?FlowDropPipelineInterface {
    $pipelineStorage = $this->entityTypeManager->getStorage("flowdrop_pipeline");

    // First try loading by entity ID.
    $pipeline = $pipelineStorage->load($pipeline_id);
    if ($pipeline instanceof FlowDropPipelineInterface) {
      return $pipeline;
    }

    // Try loading by UUID.
    $entities = $pipelineStorage->loadByProperties(["uuid" => $pipeline_id]);
    $pipeline = reset($entities);

    return $pipeline instanceof FlowDropPipelineInterface ? $pipeline : NULL;
  }

  /**
   * Finds the interrupted job and resets it to pending.
   *
   * @param \Drupal\flowdrop_pipeline\Entity\FlowDropPipelineInterface $pipeline
   *   The pipeline.
   * @param string $nodeId
   *   The node ID.
   * @param string $interruptId
   *   The interrupt ID (UUID).
   *
   * @return \Drupal\flowdrop_job\FlowDropJobInterface|null
   *   The job that was reset, or NULL if not found.
   */
  protected function findAndResetInterruptedJob(
    FlowDropPipelineInterface $pipeline,
    string $nodeId,
    string $interruptId,
  ): ?FlowDropJobInterface {
    $jobs = $pipeline->getJobs();

    foreach ($jobs as $job) {
      // Check if this job matches the interrupted node.
      if ($job->getNodeId() !== $nodeId) {
        continue;
      }

      if ($job->getStatus() !== "interrupted") {
        continue;
      }

      // Verify this job was waiting for this specific interrupt.
      $metadata = $job->getMetadata();
      $jobInterruptId = $metadata["interrupt_id"] ?? NULL;

      // Match if interrupt IDs match, or if no interrupt ID was stored
      // (backward compatibility with jobs interrupted before this fix).
      if ($jobInterruptId !== NULL && $jobInterruptId !== $interruptId) {
        $this->logger->debug("Job @job has different interrupt ID (@stored vs @current)", [
          "@job" => $job->id(),
          "@stored" => $jobInterruptId,
          "@current" => $interruptId,
        ]);
        continue;
      }

      // Found the job - reset it to pending.
      // IMPORTANT: Keep interrupt_id in metadata so the orchestrator can inject
      // it as __interrupt_id__ when the job re-executes. This allows the node
      // processor to find the resolved interrupt instead of creating a new one.
      $this->logger->info("Resetting interrupted job @job_id to pending for resume (interrupt_id: @interrupt_id)", [
        "@job_id" => $job->id(),
        "@interrupt_id" => $interruptId,
      ]);

      // Clear only the interrupt_message, keep interrupt_id for re-execution.
      unset($metadata["interrupt_message"]);
      $job->setMetadata($metadata);
      $job->markAsPending();
      $job->save();

      return $job;
    }

    return NULL;
  }

}
