<?php

declare(strict_types=1);

namespace Drupal\flowdrop_session\Service;

use Drupal\Core\DestructableInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\flowdrop_session\Constants\MessageStatus;
use Drupal\flowdrop_session\Constants\SessionStatus;
use Drupal\flowdrop_session\Entity\FlowDropSessionMessageInterface;
use Psr\Log\LoggerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;

/**
 * Handles deferred processing of session messages.
 *
 * This service processes session messages after the HTTP response has been
 * sent to the client. It implements DestructableInterface and is tagged with
 * 'needs_destruction', which means destruct() is only called if the service
 * was actually used during the request.
 *
 * This approach is more efficient than using kernel.terminate event because:
 * - The service is lazy-loaded (only instantiated when needed).
 * - destruct() is only called if messages were actually queued.
 * - No overhead on requests that don't use sessions.
 *
 * Note: This service uses lazy loading for SessionExecutionService to avoid
 * circular dependency (SessionService -> DeferredMessageProcessor).
 *
 * @internal This class is not part of the public API and may change without notice.
 */
class DeferredMessageProcessor implements DestructableInterface, DeferredSchedulerInterface {

  /**
   * User message IDs scheduled for deferred processing.
   *
   * @var array<int|string>
   */
  protected array $pendingMessageIds = [];

  /**
   * Constructs a DeferredMessageProcessor object.
   *
   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager
   *   The entity type manager.
   * @param \Symfony\Component\DependencyInjection\ContainerInterface $container
   *   The service container (used for lazy loading SessionExecutionService).
   * @param \Psr\Log\LoggerInterface $logger
   *   The logger.
   */
  public function __construct(
    protected readonly EntityTypeManagerInterface $entityTypeManager,
    protected readonly ContainerInterface $container,
    protected readonly LoggerInterface $logger,
  ) {}

  /**
   * Schedules a user message for deferred processing.
   *
   * The message will be processed after the HTTP response is sent.
   * This allows the API to return immediately while the workflow
   * executes in the background.
   *
   * @param \Drupal\flowdrop_session\Entity\FlowDropSessionMessageInterface $message
   *   The user message to process.
   */
  public function schedule(FlowDropSessionMessageInterface $message): void {
    $messageId = $message->id();
    if ($messageId !== NULL) {
      $this->pendingMessageIds[] = $messageId;
      $this->logger->info('Scheduled deferred processing for message @id', [
        '@id' => $messageId,
      ]);
    }
  }

  /**
   * Checks if there are pending messages to process.
   *
   * @return bool
   *   TRUE if there are pending messages, FALSE otherwise.
   */
  public function hasPendingMessages(): bool {
    return !empty($this->pendingMessageIds);
  }

  /**
   * {@inheritdoc}
   *
   * Processes all pending messages after the HTTP response is sent.
   * This method is called by Drupal's destruction mechanism because
   * this service is tagged with 'needs_destruction'.
   */
  public function destruct(): void {
    if (empty($this->pendingMessageIds)) {
      return;
    }

    // Close the connection early if possible (PHP-FPM/FastCGI).
    // This ensures the client receives the response immediately.
    if (function_exists('fastcgi_finish_request')) {
      fastcgi_finish_request();
    }

    $this->logger->info('Processing @count deferred message(s)', [
      '@count' => count($this->pendingMessageIds),
    ]);

    // Get the message storage.
    $messageStorage = $this->entityTypeManager
      ->getStorage('flowdrop_session_message');

    // Process each pending message.
    foreach ($this->pendingMessageIds as $messageId) {
      $this->processMessage($messageStorage, $messageId);
    }

    // Clear the pending list.
    $this->pendingMessageIds = [];
  }

  /**
   * Gets the message processor (lazy loaded to avoid circular dependency).
   *
   * @return \Drupal\flowdrop_session\Service\MessageProcessorInterface
   *   The message processor.
   */
  protected function getMessageProcessor(): MessageProcessorInterface {
    $service = $this->container->get('flowdrop_session.execution_service');
    if (!$service instanceof MessageProcessorInterface) {
      throw new \RuntimeException('flowdrop_session.execution_service must implement MessageProcessorInterface');
    }
    return $service;
  }

  /**
   * Processes a single deferred message.
   *
   * @param \Drupal\Core\Entity\EntityStorageInterface $messageStorage
   *   The message entity storage.
   * @param int|string $messageId
   *   The message ID to process.
   */
  protected function processMessage($messageStorage, int|string $messageId): void {
    try {
      // Reload the message entity to ensure we have fresh data.
      $message = $messageStorage->load($messageId);

      if (!$message instanceof FlowDropSessionMessageInterface) {
        $this->logger->warning('Message @id not found for deferred processing', [
          '@id' => $messageId,
        ]);
        return;
      }

      // Process the user message (this executes the workflow).
      $this->getMessageProcessor()->processMessage($message);

      $this->logger->info('Deferred processing completed for message @id', [
        '@id' => $messageId,
      ]);
    }
    catch (\Throwable $e) {
      $this->logger->error('Deferred processing failed for message @id: @message (@class)', [
        '@id' => $messageId,
        '@message' => $e->getMessage(),
        '@class' => get_class($e),
      ]);

      // Try to update message and session status to indicate failure.
      $this->handleProcessingFailure($messageStorage, $messageId);
    }
  }

  /**
   * Handles a processing failure by updating entity statuses.
   *
   * @param \Drupal\Core\Entity\EntityStorageInterface $messageStorage
   *   The message entity storage.
   * @param int|string $messageId
   *   The message ID that failed.
   */
  protected function handleProcessingFailure($messageStorage, int|string $messageId): void {
    try {
      $message = $messageStorage->load($messageId);
      if ($message instanceof FlowDropSessionMessageInterface) {
        $message->setStatus(MessageStatus::FAILED)->save();

        // Also update session status.
        $session = $message->getSession();
        if ($session !== NULL) {
          $session->setStatus(SessionStatus::IDLE)->save();
        }
      }
    }
    catch (\Throwable $statusException) {
      $this->logger->error('Failed to update status for message @id: @msg', [
        '@id' => $messageId,
        '@msg' => $statusException->getMessage(),
      ]);
    }
  }

}
