<?php

declare(strict_types=1);

namespace Drupal\flowdrop_interrupt\Service;

use Drupal\flowdrop_interrupt\DTO\InterruptState;
use Drupal\flowdrop_interrupt\Enum\InterruptType;
use Drupal\flowdrop_orchestration\Exception\InterruptExceptionInterface;
use Drupal\flowdrop_job\FlowDropJobInterface;
use Drupal\flowdrop_pipeline\Entity\FlowDropPipelineInterface;
use Psr\Log\LoggerInterface;

/**
 * Drupal-based implementation of interrupt persistence.
 *
 * This implementation wraps the existing InterruptManagerInterface to provide
 * persistent storage for interrupt state. It ensures that interrupt data is
 * always stored in Drupal entities regardless of the orchestrator's primary
 * storage backend.
 *
 * When using InMemoryStorage for pipelines/jobs, this service ensures that
 * interrupt state is still persisted to the database so workflows can be
 * resumed after an interrupt.
 *
 * @internal This class is not part of the public API and may change without notice.
 */
class DrupalInterruptPersistence implements InterruptPersistenceInterface {

  /**
   * The interrupt manager.
   */
  protected InterruptManagerInterface $interruptManager;

  /**
   * The logger.
   */
  protected LoggerInterface $logger;

  /**
   * Constructs a new DrupalInterruptPersistence.
   *
   * @param \Drupal\flowdrop_interrupt\Service\InterruptManagerInterface $interruptManager
   *   The interrupt manager service.
   * @param \Psr\Log\LoggerInterface $logger
   *   The logger service.
   */
  public function __construct(
    InterruptManagerInterface $interruptManager,
    LoggerInterface $logger,
  ) {
    $this->interruptManager = $interruptManager;
    $this->logger = $logger;
  }

  /**
   * {@inheritdoc}
   */
  public function persistInterruptState(
    FlowDropPipelineInterface $pipeline,
    FlowDropJobInterface $job,
    InterruptExceptionInterface $exception,
    string $executionId,
    array $partialResults = [],
  ): void {
    $interruptData = $exception->toArray();

    // Build execution context for resumption.
    $context = [
      'execution_id' => $executionId,
      'pipeline_id' => (string) $pipeline->id(),
      'job_id' => (string) $job->id(),
      'node_id' => $job->getNodeId(),
      'workflow_id' => $pipeline->getWorkflowId(),
      'partial_results' => $partialResults,
      'pipeline_input_data' => $pipeline->getInputData(),
      'job_input_data' => $job->getInputData(),
      'job_metadata' => $job->getMetadata(),
    ];

    // Merge with execution context from exception.
    $exceptionContext = $exception->getExecutionContext();
    $context = array_merge($context, $exceptionContext);

    // Determine interrupt type.
    $interruptType = $this->mapInterruptType($exception->getInterruptType());

    // Get session ID from pipeline context.
    $pipelineContext = $pipeline->getExecutionContext();
    $sessionId = $pipelineContext['session_id'] ?? $pipelineContext['playground']['session_id'] ?? NULL;

    try {
      // The interrupt is created via InterruptManager,
      // which persists to Drupal. The InterruptException may have
      // already created the interrupt entity. This method ensures
      // the interrupt state includes all necessary context.
      $existingInterrupt = $this->interruptManager->getInterrupt($exception->getInterruptId());

      if ($existingInterrupt !== NULL) {
        // Interrupt already exists, update its context if needed.
        $this->logger->debug("Interrupt @id already exists, context preserved", [
          '@id' => $exception->getInterruptId(),
        ]);
        return;
      }

      // Create new interrupt (this shouldn't normally happen
      // as the node processor typically creates the interrupt,
      // but we handle it for safety).
      $this->interruptManager->createInterrupt(
        type: $interruptType,
        workflow_id: $pipeline->getWorkflowId(),
        node_id: $job->getNodeId(),
        message: $interruptData['message'] ?? '',
        schema: $interruptData['schema'] ?? [],
        options: $interruptData['options'] ?? [],
        context: $context,
        default_value: $interruptData['default_value'] ?? NULL,
        pipeline_id: (string) $pipeline->id(),
        session_id: $sessionId !== NULL ? (int) $sessionId : NULL,
      );

      $this->logger->info("Persisted interrupt state for interrupt @id (pipeline: @pipeline)", [
        '@id' => $exception->getInterruptId(),
        '@pipeline' => $pipeline->id(),
      ]);
    }
    catch (\Exception $e) {
      $this->logger->error("Failed to persist interrupt state: @error", [
        '@error' => $e->getMessage(),
      ]);
      throw $e;
    }
  }

  /**
   * {@inheritdoc}
   */
  public function loadInterruptState(string $interruptId): ?InterruptState {
    $interrupt = $this->interruptManager->getInterrupt($interruptId);

    if ($interrupt === NULL) {
      return NULL;
    }

    $context = $interrupt->getContext();

    return new InterruptState(
      interruptId: $interrupt->uuid(),
      interruptType: $interrupt->getType()->value,
      pipelineId: $context['pipeline_id'] ?? (string) $interrupt->getPipelineId(),
      jobId: $context['job_id'] ?? '',
      nodeId: $interrupt->getNodeId(),
      executionId: $context['execution_id'] ?? '',
      workflowId: $interrupt->getWorkflowId(),
      interruptData: [
        'message' => $interrupt->getMessage(),
        'schema' => $interrupt->getSchema(),
        'options' => $interrupt->getOptions(),
        'default_value' => $interrupt->getDefaultValue(),
      ],
      executionContext: $context,
      partialResults: $context['partial_results'] ?? [],
      status: $this->mapStatus($interrupt->getStatus()->value),
      userInput: $interrupt->getResponseData(),
      createdAt: $interrupt->getCreatedTime(),
      resolvedAt: $interrupt->getResolvedAt(),
    );
  }

  /**
   * {@inheritdoc}
   */
  public function resolveInterrupt(string $interruptId, array $userInput): void {
    $this->interruptManager->resolveInterrupt($interruptId, $userInput);
  }

  /**
   * {@inheritdoc}
   */
  public function isInterruptPending(string $interruptId): bool {
    $interrupt = $this->interruptManager->getInterrupt($interruptId);

    if ($interrupt === NULL) {
      return FALSE;
    }

    return $interrupt->getStatus()->value === 'pending';
  }

  /**
   * {@inheritdoc}
   */
  public function deleteInterruptState(string $interruptId): void {
    // We don't delete interrupts immediately - they're cleaned up by cron.
    // Instead, mark as cancelled if needed.
    $this->interruptManager->cancelInterrupt($interruptId);
  }

  /**
   * Maps interrupt type string to InterruptType enum.
   *
   * @param string $type
   *   The interrupt type string.
   *
   * @return \Drupal\flowdrop_interrupt\Enum\InterruptType
   *   The interrupt type enum.
   */
  protected function mapInterruptType(string $type): InterruptType {
    return match ($type) {
      'text_input', 'text' => InterruptType::Text,
      'choice' => InterruptType::Choice,
      'confirmation' => InterruptType::Confirmation,
      'form_input', 'form' => InterruptType::Form,
      default => InterruptType::Text,
    };
  }

  /**
   * Maps interrupt status to InterruptState status constant.
   *
   * @param string $status
   *   The interrupt entity status.
   *
   * @return string
   *   The InterruptState status constant.
   */
  protected function mapStatus(string $status): string {
    return match ($status) {
      'pending' => InterruptState::STATUS_PENDING,
      'resolved' => InterruptState::STATUS_RESOLVED,
      'expired', 'cancelled' => InterruptState::STATUS_EXPIRED,
      default => InterruptState::STATUS_PENDING,
    };
  }

}
