<?php

declare(strict_types=1);

namespace Drupal\flowdrop_interrupt\Exception;

use Drupal\flowdrop_interrupt\Entity\FlowDropInterruptInterface;
use Drupal\flowdrop_interrupt\Message\InterruptMessageInterface;
use Drupal\flowdrop_orchestration\Exception\FlowDropInterruptException;

/**
 * Exception thrown when a workflow node requires external resolution.
 *
 * Two shapes are supported:
 *
 * - **Message shape (preferred):** the node throws with an
 *   {@see InterruptMessageInterface} value object.
 *   The orchestrator's catch-side
 *   subscriber dispatches the message through the
 *   {@see \Drupal\flowdrop_interrupt\Service\InterruptMessageBusInterface},
 *   stamping execution identity, then marks the job interrupted and attaches
 *   the entity to JobInterruptCaughtEvent.
 *   The exception itself stays immutable; the persisted entity is read from
 *   the event, not from this exception.
 *
 * - **Pre-persisted entity shape:** the node creates the entity itself
 *   (synchronously, e.g. for idempotency or resume) and passes it in. The
 *   subscriber stamps any missing fields and attaches the entity to the
 *   event unchanged.
 *
 * Message example:
 * @code
 * throw new InterruptRequiredException(new HitlInterruptMessage(
 *   message: 'Approve?',
 *   schema: ['type' => 'boolean', 'presentation' => 'confirmation'],
 * ));
 * @endcode
 *
 * @internal This class is not part of the public API and may change without notice.
 */
class InterruptRequiredException extends FlowDropInterruptException {

  /**
   * The exception payload — either a pre-persisted entity or a message DTO.
   */
  public readonly FlowDropInterruptInterface|InterruptMessageInterface $payload;

  /**
   * Resume context payload.
   *
   * @var array<string, mixed>
   */
  public readonly array $executionContext;

  /**
   * Constructs a new InterruptRequiredException.
   *
   * @param \Drupal\flowdrop_interrupt\Entity\FlowDropInterruptInterface|\Drupal\flowdrop_interrupt\Message\InterruptMessageInterface $payload
   *   Either the persisted entity or a message DTO.
   * @param array<string, mixed> $executionContext
   *   Optional resume context. Plugins rarely need this; the orchestrator
   *   reconstructs upstream params on resume.
   * @param string $message
   *   Exception message.
   */
  public function __construct(
    FlowDropInterruptInterface|InterruptMessageInterface $payload,
    array $executionContext = [],
    string $message = "Workflow interrupted for user input",
  ) {
    $this->payload = $payload;
    $this->executionContext = $executionContext;
    parent::__construct($message);
  }

  /**
   * Returns the pre-persisted entity (entity shape), or NULL for message shape.
   *
   * For message-shape exceptions the persisted entity lives on the dispatched
   * JobInterruptCaughtEvent, not on this exception. The catch site reads it
   * from there.
   */
  public function getInterrupt(): ?FlowDropInterruptInterface {
    return $this->payload instanceof FlowDropInterruptInterface
      ? $this->payload
      : NULL;
  }

  /**
   * Returns the message DTO, or NULL for entity-shape exceptions.
   */
  public function getInterruptMessage(): ?InterruptMessageInterface {
    return $this->payload instanceof InterruptMessageInterface
      ? $this->payload
      : NULL;
  }

  /**
   * Returns the resume context payload, if any.
   *
   * @return array<string, mixed>
   *   The resume context.
   */
  public function getExecutionContext(): array {
    return $this->executionContext;
  }

  /**
   * {@inheritdoc}
   *
   * For entity-shape exceptions returns the entity UUID. For message-shape
   * exceptions throws — the runtime catch site should read the persisted
   * entity from JobInterruptCaughtEvent instead.
   */
  public function getInterruptId(): string {
    $entity = $this->getInterrupt();
    if ($entity === NULL) {
      throw new \LogicException(sprintf(
        'getInterruptId() called on a message-shape InterruptRequiredException '
        . '(message type: %s). The persisted entity lives on the dispatched '
        . 'JobInterruptCaughtEvent; read from the event rather than the exception.',
        $this->payload instanceof InterruptMessageInterface
          ? $this->payload->getType()->value
          : 'unknown',
      ));
    }
    return $entity->uuid() ?? '';
  }

  /**
   * {@inheritdoc}
   *
   * For entity-shape returns the entity type value. For message-shape returns
   * the type declared on the message DTO.
   */
  public function getInterruptType(): string {
    $entity = $this->getInterrupt();
    if ($entity !== NULL) {
      return $entity->getType()->value;
    }
    if ($this->payload instanceof InterruptMessageInterface) {
      return $this->payload->getType()->value;
    }
    return '';
  }

  /**
   * {@inheritdoc}
   *
   * For entity-shape serializes the entity. For message-shape returns a
   * minimal serialization based on the message; the full entity is available
   * on JobInterruptCaughtEvent.
   *
   * @return array<string, mixed>
   *   The serialized form.
   */
  public function toArray(): array {
    $entity = $this->getInterrupt();
    if ($entity !== NULL) {
      return [
        "interrupt" => $entity->toApiArray(),
        "context" => $this->executionContext,
      ];
    }
    $message = $this->getInterruptMessage();
    return [
      "message" => $message !== NULL ? [
        "type" => $message->getType()->value,
        "message" => $message->getMessage(),
      ] : NULL,
      "context" => $this->executionContext,
    ];
  }

}
