<?php

declare(strict_types=1);

namespace Drupal\flowdrop_runtime\Service\Orchestrator;

use Drupal\flowdrop_interrupt\Entity\FlowDropInterruptInterface;
use Drupal\flowdrop_orchestration\DTO\Orchestrator\OrchestrationResponse;
use Drupal\flowdrop_orchestration\DTO\Snapshot\WorkflowSnapshot;
use Drupal\flowdrop_orchestration\Exception\InterruptExceptionInterface;
use Drupal\flowdrop_runtime\Event\WorkflowInterruptedEvent;
use Drupal\flowdrop_runtime\Service\RealTime\RealTimeManagerInterface;
use Psr\Log\LoggerInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;

/**
 * Provides common interrupt handling logic for orchestrators.
 *
 * This trait standardizes how orchestrators handle InterruptExceptionInterface
 * exceptions, ensuring consistent behavior across synchronous, asynchronous,
 * pipeline-based, and stategraph orchestrators.
 *
 * Requirements for using this trait:
 * - The class must implement OrchestratorInterface.
 * - The class must have access to RealTimeManager, EventDispatcher, and Logger.
 *
 * @see \Drupal\flowdrop_orchestration\Exception\InterruptExceptionInterface
 *
 * @internal This trait is not part of the public API and may change without notice.
 */
trait InterruptHandlingTrait {

  /**
   * Handles an interrupt exception and returns an appropriate response.
   *
   * This method:
   * - Updates the execution status to "interrupted".
   * - Logs the interrupt event.
   * - Dispatches the "flowdrop.workflow.interrupted" event.
   * - Returns an OrchestrationResponse with status "interrupted".
   *
   * @param \Drupal\flowdrop_orchestration\Exception\InterruptExceptionInterface $exception
   *   The interrupt exception that was thrown.
   * @param string $executionId
   *   The execution ID for this workflow run.
   * @param string $workflowId
   *   The workflow ID being executed.
   * @param float $startTime
   *   The microtime when execution started.
   * @param array<string, mixed> $results
   *   The results collected so far during execution.
   * @param array<string, mixed> $additionalMetadata
   *   Additional metadata to include in the response.
   * @param \Drupal\flowdrop_interrupt\Entity\FlowDropInterruptInterface|null $persistedInterrupt
   *   The persisted interrupt entity, when already resolved by the catch-side
   *   subscriber.
   *
   * @return \Drupal\flowdrop_orchestration\DTO\Orchestrator\OrchestrationResponse
   *   The orchestration response with "interrupted" status.
   */
  protected function handleInterruptException(
    InterruptExceptionInterface $exception,
    string $executionId,
    string $workflowId,
    float $startTime,
    array $results = [],
    array $additionalMetadata = [],
    ?FlowDropInterruptInterface $persistedInterrupt = NULL,
  ): OrchestrationResponse {
    $totalTime = microtime(TRUE) - $startTime;

    // Resolve interrupt identity. For message-shape InterruptRequiredException
    // the entity isn't on the exception (`getInterruptId()` would throw a
    // LogicException) — it's been persisted by JobInterruptCaughtSubscriber
    // and reaches us via $persistedInterrupt. Prefer that; fall back to the
    // interface method when not provided, swallowing the LogicException
    // rather than escalating it into a fake "orchestration failed".
    [$interruptId, $interruptType, $interruptPayload] = $this->resolveInterruptIdentity(
      $exception,
      $persistedInterrupt,
    );

    // Update execution status to interrupted.
    $this->getInterruptRealTimeManager()->updateExecutionStatus($executionId, WorkflowSnapshot::STATUS_INTERRUPTED, [
      "interrupt_id" => $interruptId,
      "interrupt_type" => $interruptType,
      "total_execution_time" => $totalTime,
      "nodes_executed" => count($results),
      "end_time" => time(),
    ]);

    // Log workflow interrupted.
    $this->getInterruptLogger()->info("Workflow @workflow interrupted at node for user input. Interrupt ID: @interrupt", [
      "@workflow" => $workflowId,
      "@interrupt" => $interruptId,
    ]);

    // Emit workflow interrupted event.
    $this->getInterruptEventDispatcher()->dispatch(
      new WorkflowInterruptedEvent(
        executionId: $executionId,
        workflowId: $workflowId,
        interrupt: $interruptPayload,
        interruptContext: $exception->getExecutionContext(),
      ),
      WorkflowInterruptedEvent::NAME
    );

    // Build metadata for the response.
    $metadata = array_merge([
      "orchestrator_type" => $this->getType(),
      "nodes_executed" => count($results),
      "interrupt" => $interruptPayload,
    ], $additionalMetadata);

    return new OrchestrationResponse(
      executionId: $executionId,
      status: WorkflowSnapshot::STATUS_INTERRUPTED,
      results: $results,
      executionTime: $totalTime,
      metadata: $metadata,
    );
  }

  /**
   * Resolves the interrupt id, type, and serialized payload.
   *
   * Handles both shapes of InterruptRequiredException:
   *   - **Entity shape** — the entity is on the exception; serialize it.
   *   - **Message shape** — the entity has been persisted by
   *     JobInterruptCaughtSubscriber and is passed in via $persistedInterrupt.
   *     Use it to build downstream-compatible metadata
   *     (`interrupt.interrupt.id` etc., which SessionExecutionService reads
   *     on the response).
   *
   * If neither is available (e.g. a third-party orchestrator's stateless
   * interrupt that never went through the bus), fall back to whatever the
   * exception itself can offer, swallowing LogicException so we degrade
   * gracefully rather than masking the interrupt as a fatal error.
   *
   * @return array{0: string, 1: string, 2: array<string, mixed>}
   *   Tuple of [id, type, serialized_payload].
   */
  private function resolveInterruptIdentity(
    InterruptExceptionInterface $exception,
    ?FlowDropInterruptInterface $persistedInterrupt,
  ): array {
    if ($persistedInterrupt !== NULL) {
      return [
        (string) ($persistedInterrupt->uuid() ?? ''),
        $persistedInterrupt->getType()->value,
        [
          'interrupt' => $persistedInterrupt->toApiArray(),
          'context' => $exception->getExecutionContext(),
        ],
      ];
    }
    try {
      $id = $exception->getInterruptId();
    }
    catch (\LogicException) {
      $id = '';
    }
    try {
      $type = $exception->getInterruptType();
    }
    catch (\Throwable) {
      $type = '';
    }
    return [$id, $type, $exception->toArray()];
  }

  /**
   * Gets the real-time manager for interrupt handling.
   *
   * Override this method if the property name differs in your class.
   *
   * @return \Drupal\flowdrop_runtime\Service\RealTime\RealTimeManagerInterface
   *   The real-time manager service.
   */
  protected function getInterruptRealTimeManager(): RealTimeManagerInterface {
    // @phpstan-ignore-next-line - Property exists in implementing class.
    return $this->realTimeManager;
  }

  /**
   * Gets the event dispatcher for interrupt handling.
   *
   * Override this method if the property name differs in your class.
   *
   * @return \Symfony\Component\EventDispatcher\EventDispatcherInterface
   *   The event dispatcher service.
   */
  protected function getInterruptEventDispatcher(): EventDispatcherInterface {
    // @phpstan-ignore-next-line - Property exists in implementing class.
    return $this->eventDispatcher;
  }

  /**
   * Gets the logger for interrupt handling.
   *
   * Override this method if the property name differs in your class.
   *
   * @return \Psr\Log\LoggerInterface
   *   The logger service.
   */
  protected function getInterruptLogger(): LoggerInterface {
    // @phpstan-ignore-next-line - Property exists in implementing class.
    return $this->logger;
  }

}
