<?php

declare(strict_types=1);

namespace Drupal\flowdrop_session\Service;

use Drupal\Component\Serialization\Json;
use Drupal\Core\Database\Connection;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Logger\LoggerChannelFactoryInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\flowdrop_session\Constants\MessageStatus;
use Drupal\flowdrop_session\Constants\SessionStatus;
use Drupal\flowdrop_session\Entity\FlowDropSessionMessage;
use Drupal\flowdrop_session\Entity\FlowDropSessionMessageInterface;
use Drupal\flowdrop_session\Entity\FlowDropSession;
use Drupal\flowdrop_session\Entity\FlowDropSessionInterface;
use Drupal\flowdrop_workflow\FlowDropWorkflowInterface;
use Psr\Log\LoggerInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Drupal\Core\Queue\QueueFactory;

/**
 * Service for managing sessions and message execution.
 *
 * This service handles the core business logic for:
 * - Creating and managing sessions
 * - Processing user messages and triggering workflow execution
 * - Managing the chat-like interface between user and workflow.
 *
 * @internal This class is not part of the public API and may change without notice.
 */
class SessionService {

  /**
   * The entity type manager.
   *
   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
   */
  protected EntityTypeManagerInterface $entityTypeManager;

  /**
   * The current user.
   *
   * @var \Drupal\Core\Session\AccountInterface
   */
  protected AccountInterface $currentUser;

  /**
   * The logger.
   *
   * @var \Psr\Log\LoggerInterface
   */
  protected LoggerInterface $logger;

  /**
   * The event dispatcher.
   *
   * @var \Symfony\Component\EventDispatcher\EventDispatcherInterface
   */
  protected EventDispatcherInterface $eventDispatcher;

  /**
   * The session entity storage.
   *
   * @var \Drupal\Core\Entity\EntityStorageInterface
   */
  protected EntityStorageInterface $sessionStorage;

  /**
   * The message entity storage.
   *
   * @var \Drupal\Core\Entity\EntityStorageInterface
   */
  protected EntityStorageInterface $messageStorage;

  /**
   * The queue factory.
   *
   * @var \Drupal\Core\Queue\QueueFactory
   */
  protected QueueFactory $queueFactory;

  /**
   * The database connection.
   *
   * @var \Drupal\Core\Database\Connection
   */
  protected Connection $database;

  /**
   * The deferred message scheduler.
   *
   * @var \Drupal\flowdrop_session\Service\DeferredSchedulerInterface
   */
  protected DeferredSchedulerInterface $deferredMessageProcessor;

  /**
   * Constructs a new SessionService.
   *
   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
   *   The entity type manager.
   * @param \Drupal\Core\Session\AccountInterface $current_user
   *   The current user.
   * @param \Drupal\Core\Logger\LoggerChannelFactoryInterface $logger_factory
   *   The logger factory.
   * @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $event_dispatcher
   *   The event dispatcher.
   * @param \Drupal\Core\Queue\QueueFactory $queue_factory
   *   The queue factory.
   * @param \Drupal\Core\Database\Connection $database
   *   The database connection.
   * @param \Drupal\flowdrop_session\Service\DeferredSchedulerInterface $deferred_message_processor
   *   The deferred message scheduler.
   */
  public function __construct(
    EntityTypeManagerInterface $entity_type_manager,
    AccountInterface $current_user,
    LoggerChannelFactoryInterface $logger_factory,
    EventDispatcherInterface $event_dispatcher,
    QueueFactory $queue_factory,
    Connection $database,
    DeferredSchedulerInterface $deferred_message_processor,
  ) {
    $this->entityTypeManager = $entity_type_manager;
    $this->currentUser = $current_user;
    $this->logger = $logger_factory->get('flowdrop_session');
    $this->eventDispatcher = $event_dispatcher;
    $this->queueFactory = $queue_factory;
    $this->database = $database;
    $this->deferredMessageProcessor = $deferred_message_processor;
    $this->sessionStorage = $entity_type_manager->getStorage('flowdrop_session');
    $this->messageStorage = $entity_type_manager->getStorage('flowdrop_session_message');
  }

  /**
   * Creates a new session for a workflow.
   *
   * @param \Drupal\flowdrop_workflow\FlowDropWorkflowInterface $workflow
   *   The workflow entity.
   * @param string|null $name
   *   Optional session name. If not provided or if it matches the "Session N"
   *   pattern (which may be stale from frontend), a unique name is generated.
   * @param array<string, mixed> $metadata
   *   Optional metadata for the session.
   *
   * @return \Drupal\flowdrop_session\Entity\FlowDropSessionInterface
   *   The created session entity.
   *
   * @throws \Drupal\Core\Entity\EntityStorageException
   *   If the entity cannot be saved.
   */
  public function createSession(
    FlowDropWorkflowInterface $workflow,
    ?string $name = NULL,
    array $metadata = [],
  ): FlowDropSessionInterface {
    // Generate a unique session name based on the actual database count.
    // This fixes a frontend bug where the session counter can get stale,
    // resulting in duplicate names like "Session 21" for all new sessions.
    $sessionName = $this->generateUniqueSessionName($name);

    $session = FlowDropSession::create([
      'workflow_id' => $workflow->id(),
      'name' => $sessionName,
      'status' => SessionStatus::IDLE,
      'uid' => $this->currentUser->id(),
      // JSON-encode metadata for string_long field type.
      // Passing an array directly would be cast to the string "Array".
      'metadata' => Json::encode($metadata),
    ]);

    $session->save();

    $this->logger->info('Created session @id for workflow @workflow', [
      '@id' => $session->id(),
      '@workflow' => $workflow->id(),
    ]);

    return $session;
  }

  /**
   * Generates a unique session name based on the database count.
   *
   * If the provided name is NULL or matches the "Session N" pattern (which may
   * be stale from frontend), generates a new unique name based on the actual
   * count of sessions in the database.
   *
   * @param string|null $name
   *   The proposed session name, or NULL.
   *
   * @return string
   *   A unique session name.
   */
  protected function generateUniqueSessionName(?string $name): string {
    // If no name provided, or if name matches "Session N" pattern (potentially
    // stale from frontend counter bug), generate a unique name from DB count.
    $shouldGenerate = $name === NULL || preg_match('/^Session \d+$/', $name);

    if ($shouldGenerate) {
      $count = $this->sessionStorage->getQuery()
        ->accessCheck(TRUE)
        ->count()
        ->execute();
      return "Session " . ($count + 1);
    }

    return $name;
  }

  /**
   * Gets sessions for a workflow.
   *
   * @param string $workflow_id
   *   The workflow ID.
   * @param int|null $user_id
   *   Optional user ID to filter by owner.
   * @param int $limit
   *   Maximum number of sessions to return.
   * @param int $offset
   *   Number of sessions to skip.
   *
   * @return array<\Drupal\flowdrop_session\Entity\FlowDropSessionInterface>
   *   Array of session entities.
   */
  public function getSessionsForWorkflow(
    string $workflow_id,
    ?int $user_id = NULL,
    int $limit = 20,
    int $offset = 0,
  ): array {
    $query = $this->sessionStorage->getQuery()
      ->condition('workflow_id', $workflow_id)
      ->accessCheck(TRUE)
      ->sort('created', 'DESC')
      ->range($offset, $limit);

    if ($user_id !== NULL) {
      $query->condition('uid', $user_id);
    }

    $ids = $query->execute();

    if (empty($ids)) {
      return [];
    }

    $sessions = $this->sessionStorage->loadMultiple($ids);
    $result = [];
    foreach ($sessions as $session) {
      if ($session instanceof FlowDropSessionInterface) {
        $result[] = $session;
      }
    }

    return $result;
  }

  /**
   * Gets a session by ID.
   *
   * @param int $session_id
   *   The session ID.
   *
   * @return \Drupal\flowdrop_session\Entity\FlowDropSessionInterface|null
   *   The session entity or NULL if not found.
   */
  public function getSession(int $session_id): ?FlowDropSessionInterface {
    $entity = $this->sessionStorage->load($session_id);
    return $entity instanceof FlowDropSessionInterface ? $entity : NULL;
  }

  /**
   * Checks if a workflow has a chat input node.
   *
   * Chat input nodes allow users to type messages in the playground.
   * Workflows without chat input nodes should hide the text input and
   * only show the Run button.
   *
   * @param \Drupal\flowdrop_workflow\FlowDropWorkflowInterface $workflow
   *   The workflow entity.
   *
   * @return bool
   *   TRUE if the workflow has a chat input node, FALSE otherwise.
   */
  public function hasChatInputNode(FlowDropWorkflowInterface $workflow): bool {
    return $this->findChatInputNode($workflow) !== NULL;
  }

  /**
   * Deletes a session and all its messages.
   *
   * @param \Drupal\flowdrop_session\Entity\FlowDropSessionInterface $session
   *   The session to delete.
   *
   * @throws \Drupal\Core\Entity\EntityStorageException
   *   If deletion fails.
   */
  public function deleteSession(FlowDropSessionInterface $session): void {
    $session_id = $session->id();
    if ($session_id === NULL) {
      return;
    }

    // Delete all messages for this session.
    $message_ids = $this->messageStorage->getQuery()
      ->condition('session_id', $session_id)
      ->accessCheck(TRUE)
      ->execute();

    if (!empty($message_ids)) {
      $messages = $this->messageStorage->loadMultiple($message_ids);
      $this->messageStorage->delete($messages);
    }

    // Delete the session.
    $session->delete();

    $this->logger->info('Deleted session @id with @count messages', [
      '@id' => $session_id,
      '@count' => count($message_ids),
    ]);
  }

  /**
   * Gets messages for a session.
   *
   * @param int $session_id
   *   The session ID.
   * @param int|null $since_timestamp
   *   Optional timestamp to fetch messages after.
   * @param int $limit
   *   Maximum number of messages to return.
   *
   * @return array<\Drupal\flowdrop_session\Entity\FlowDropSessionMessageInterface>
   *   Array of message entities.
   */
  public function getMessages(
    int $session_id,
    ?int $since_timestamp = NULL,
    int $limit = 100,
  ): array {
    $query = $this->messageStorage->getQuery()
      ->condition('session_id', $session_id)
      ->accessCheck(TRUE)
      ->sort('timestamp', 'ASC')
      ->sort('id', 'ASC')
      ->range(0, $limit);

    if ($since_timestamp !== NULL) {
      // Use >= to include messages created in the same second.
      // The frontend should deduplicate by message ID.
      // This prevents race conditions where assistant messages are
      // created in the same second as user messages during deferred
      // processing and would otherwise be filtered out by '>'.
      $query->condition('timestamp', $since_timestamp, '>=');
    }

    $ids = $query->execute();

    if (empty($ids)) {
      return [];
    }

    $messages = $this->messageStorage->loadMultiple($ids);
    $result = [];
    foreach ($messages as $message) {
      if ($message instanceof FlowDropSessionMessageInterface) {
        $result[] = $message;
      }
    }

    return $result;
  }

  /**
   * Adds a user message to the session.
   *
   * @param \Drupal\flowdrop_session\Entity\FlowDropSessionInterface $session
   *   The session entity.
   * @param string $content
   *   The message content.
   * @param array<string, mixed> $metadata
   *   Optional metadata.
   *
   * @return \Drupal\flowdrop_session\Entity\FlowDropSessionMessageInterface
   *   The created message entity.
   *
   * @throws \Drupal\Core\Entity\EntityStorageException
   *   If the entity cannot be saved.
   */
  public function addUserMessage(
    FlowDropSessionInterface $session,
    string $content,
    array $metadata = [],
  ): FlowDropSessionMessageInterface {
    return $this->createMessage($session, 'user', $content, NULL, $metadata);
  }

  /**
   * Adds an assistant message to the session.
   *
   * @param \Drupal\flowdrop_session\Entity\FlowDropSessionInterface $session
   *   The session entity.
   * @param string $content
   *   The message content.
   * @param string|null $node_id
   *   Optional node ID that generated this message.
   * @param array<string, mixed> $metadata
   *   Optional metadata.
   *
   * @return \Drupal\flowdrop_session\Entity\FlowDropSessionMessageInterface
   *   The created message entity.
   *
   * @throws \Drupal\Core\Entity\EntityStorageException
   *   If the entity cannot be saved.
   */
  public function addAssistantMessage(
    FlowDropSessionInterface $session,
    string $content,
    ?string $node_id = NULL,
    array $metadata = [],
  ): FlowDropSessionMessageInterface {
    return $this->createMessage($session, 'assistant', $content, $node_id, $metadata);
  }

  /**
   * Adds a system message to the session.
   *
   * @param \Drupal\flowdrop_session\Entity\FlowDropSessionInterface $session
   *   The session entity.
   * @param string $content
   *   The message content.
   * @param array<string, mixed> $metadata
   *   Optional metadata.
   *
   * @return \Drupal\flowdrop_session\Entity\FlowDropSessionMessageInterface
   *   The created message entity.
   *
   * @throws \Drupal\Core\Entity\EntityStorageException
   *   If the entity cannot be saved.
   */
  public function addSystemMessage(
    FlowDropSessionInterface $session,
    string $content,
    array $metadata = [],
  ): FlowDropSessionMessageInterface {
    return $this->createMessage($session, 'system', $content, NULL, $metadata);
  }

  /**
   * Adds a log message to the session.
   *
   * @param \Drupal\flowdrop_session\Entity\FlowDropSessionInterface $session
   *   The session entity.
   * @param string $content
   *   The log message content.
   * @param string|null $node_id
   *   The node ID this log relates to.
   * @param string $level
   *   The log level (info, warning, error, debug).
   * @param array<string, mixed> $metadata
   *   Optional additional metadata.
   *
   * @return \Drupal\flowdrop_session\Entity\FlowDropSessionMessageInterface
   *   The created message entity.
   *
   * @throws \Drupal\Core\Entity\EntityStorageException
   *   If the entity cannot be saved.
   */
  public function addLogMessage(
    FlowDropSessionInterface $session,
    string $content,
    ?string $node_id = NULL,
    string $level = 'info',
    array $metadata = [],
  ): FlowDropSessionMessageInterface {
    $metadata['level'] = $level;
    return $this->createMessage($session, 'log', $content, $node_id, $metadata);
  }

  /**
   * Creates a message entity.
   *
   * @param \Drupal\flowdrop_session\Entity\FlowDropSessionInterface $session
   *   The session entity.
   * @param string $role
   *   The message role.
   * @param string $content
   *   The message content.
   * @param string|null $node_id
   *   Optional node ID.
   * @param array<string, mixed> $metadata
   *   Optional metadata.
   *
   * @return \Drupal\flowdrop_session\Entity\FlowDropSessionMessageInterface
   *   The created message entity.
   *
   * @throws \Drupal\Core\Entity\EntityStorageException
   *   If the entity cannot be saved.
   */
  protected function createMessage(
    FlowDropSessionInterface $session,
    string $role,
    string $content,
    ?string $node_id = NULL,
    array $metadata = [],
  ): FlowDropSessionMessageInterface {
    // Extract sequence_number from metadata if provided.
    $sequenceNumber = $metadata['sequence_number'] ?? NULL;
    unset($metadata['sequence_number']);

    // Always assign a sequence number for proper message ordering.
    // If not provided, get the next sequence number automatically.
    if ($sequenceNumber === NULL) {
      $sequenceNumber = $this->getNextSequenceNumber($session);
    }

    // For user messages, store the current user's display name in metadata.
    // This allows the frontend to show the actual user name instead of "You".
    if ($role === 'user' && !isset($metadata['userName'])) {
      $metadata['userName'] = $this->currentUser->getDisplayName();
    }

    // Encode metadata as JSON string - the field is string_long type.
    $metadataJson = !empty($metadata) ? Json::encode($metadata) : '{}';

    // Determine initial status based on message role and type.
    // User messages start as 'pending' since they trigger workflow execution.
    // Interrupt request messages must also be 'pending' so the frontend knows
    // the user still needs to interact with them (buttons should be enabled).
    // Other system/assistant/log messages are 'completed' immediately since
    // they don't require processing and shouldn't block the UI.
    $isInterruptRequest = ($metadata['type'] ?? NULL) === 'interrupt_request';
    if ($role === 'user' || $isInterruptRequest) {
      $initialStatus = MessageStatus::PENDING;
    }
    else {
      $initialStatus = MessageStatus::COMPLETED;
    }

    $message = FlowDropSessionMessage::create([
      'session_id' => $session->id(),
      'role' => $role,
      'content' => $content,
      'node_id' => $node_id,
      'metadata' => $metadataJson,
      'status' => $initialStatus,
    ]);

    // Always set sequence number for proper ordering.
    $message->setSequenceNumber((int) $sequenceNumber);

    $message->save();

    // Update session's changed timestamp.
    $session->save();

    return $message;
  }

  /**
   * Sends a message and triggers workflow execution.
   *
   * This is the main entry point for processing user input in the session.
   * It creates a user message with transaction safety, validates sequence
   * ordering, and triggers workflow execution (sync or async based on session
   * configuration).
   *
   * @param \Drupal\flowdrop_session\Entity\FlowDropSessionInterface $session
   *   The session entity.
   * @param string $content
   *   The user message content.
   * @param array<string, mixed> $inputs
   *   Additional input values for workflow nodes.
   *
   * @return \Drupal\flowdrop_session\Entity\FlowDropSessionMessageInterface
   *   The created user message entity.
   *
   * @throws \Exception
   *   If execution fails.
   */
  public function sendMessage(
    FlowDropSessionInterface $session,
    string $content,
    array $inputs = [],
  ): FlowDropSessionMessageInterface {
    $transaction = $this->database->startTransaction();
    try {
      // Get next sequence number for user message.
      $userSequence = $this->getNextSequenceNumber($session);

      // Validate sequence ordering (block if previous message not complete).
      $this->validateSequenceOrder($session, $userSequence);

      // Create user message.
      $userMessage = $this->createMessage($session, 'user', $content, NULL, [
        'sequence_number' => $userSequence,
        'inputs' => $inputs,
      ]);

      // Get execution mode to determine processing strategy.
      $executionMode = $session->getExecutionMode();

      if ($executionMode !== 'async') {
        // Sync mode with deferred processing:
        // - User message is "completed" (shown immediately in UI)
        // - Session is "running" (triggers frontend polling)
        // - Processing happens after HTTP response is sent.
        $userMessage->setStatus(MessageStatus::COMPLETED)->save();
        $session->setStatus(SessionStatus::RUNNING)->save();

        // Schedule for deferred processing after HTTP response.
        $this->scheduleDeferredSyncProcessing($userMessage);
      }
      else {
        // Async mode: queue for later processing by cron/queue worker.
        $userMessage->setStatus(MessageStatus::PENDING)->save();
        $this->queueMessageProcessing($userMessage);
      }

      return $userMessage;
    }
    catch (\Exception $e) {
      $transaction->rollback();
      $this->logger->error('Failed to send message for session @id: @message', [
        '@id' => $session->id(),
        '@message' => $e->getMessage(),
      ]);
      throw $e;
    }
  }

  /**
   * Schedules a user message for deferred synchronous processing.
   *
   * The message will be processed after the HTTP response is sent using
   * the DeferredMessageProcessor service (tagged with 'needs_destruction').
   * This allows the API to return immediately while the workflow executes
   * in the background, and only adds overhead when actually used.
   *
   * @param \Drupal\flowdrop_session\Entity\FlowDropSessionMessageInterface $message
   *   The user message to process.
   */
  protected function scheduleDeferredSyncProcessing(
    FlowDropSessionMessageInterface $message,
  ): void {
    $this->deferredMessageProcessor->schedule($message);
  }

  /**
   * Stops the execution of a session.
   *
   * @param \Drupal\flowdrop_session\Entity\FlowDropSessionInterface $session
   *   The session entity.
   *
   * @throws \RuntimeException
   *   If no execution is running.
   */
  public function stopExecution(FlowDropSessionInterface $session): void {
    if (!$session->isRunning()) {
      throw new \RuntimeException('No execution is running for this session.');
    }

    $pipeline_id = $session->getCurrentPipelineId();

    if ($pipeline_id !== NULL) {
      try {
        $pipeline_storage = $this->entityTypeManager->getStorage('flowdrop_pipeline');
        $pipeline = $pipeline_storage->load($pipeline_id);

        if ($pipeline !== NULL && method_exists($pipeline, 'markAsCancelled')) {
          $pipeline->markAsCancelled();
          $pipeline->save();
        }
      }
      catch (\Exception $e) {
        $this->logger->warning('Failed to cancel pipeline @id: @message', [
          '@id' => $pipeline_id,
          '@message' => $e->getMessage(),
        ]);
      }
    }

    // Update session status.
    $session->setStatus(SessionStatus::IDLE);
    $session->setCurrentPipelineId(NULL);
    $session->save();

    // Add system message.
    $this->addSystemMessage($session, 'Execution stopped by user.');

    $this->logger->info('Stopped execution for session @id', [
      '@id' => $session->id(),
    ]);
  }

  /**
   * Resets a session to idle state, recovering from stuck states.
   *
   * This method is useful when a session gets stuck due to failed executions
   * that didn't properly cleanup, network issues during processing, or
   * unexpected errors that left the session in a non-terminal state.
   *
   * It will reset session status to 'idle', mark any non-terminal messages
   * as 'cancelled', clear any pending interrupt data, and add a system
   * message indicating the reset.
   *
   * @param \Drupal\flowdrop_session\Entity\FlowDropSessionInterface $session
   *   The session entity to reset.
   *
   * @return int
   *   The number of messages that were reset/cancelled.
   */
  public function resetSession(FlowDropSessionInterface $session): int {
    $sessionId = $session->id();
    if ($sessionId === NULL) {
      return 0;
    }

    $resetCount = 0;

    // Find and cancel any non-terminal messages.
    $query = $this->messageStorage->getQuery()
      ->condition('session_id', $sessionId)
      ->condition('status', [MessageStatus::PENDING, MessageStatus::PROCESSING, MessageStatus::AWAITING_INPUT], 'IN')
      ->accessCheck(TRUE);

    $messageIds = $query->execute();

    if (!empty($messageIds)) {
      $messages = $this->messageStorage->loadMultiple($messageIds);
      foreach ($messages as $message) {
        if ($message instanceof FlowDropSessionMessageInterface) {
          $message->setStatus(MessageStatus::CANCELLED)->save();
          $resetCount++;
        }
      }
    }

    // Reset session status to idle.
    $session->setStatus(SessionStatus::IDLE);
    $session->setCurrentPipelineId(NULL);

    // Clear any pending interrupt metadata.
    $metadata = $session->getMetadata();
    unset($metadata['pending_interrupt_id']);
    unset($metadata['pending_interrupt_type']);
    unset($metadata['pending_execution_id']);
    $session->setMetadata($metadata);
    $session->save();

    // Add system message indicating the reset.
    if ($resetCount > 0) {
      $this->addSystemMessage(
        $session,
        "Session reset: {$resetCount} pending message(s) cancelled. You can continue the conversation.",
        ['level' => 'info', 'type' => 'session_reset']
      );
    }
    else {
      $this->addSystemMessage(
        $session,
        "Session reset to idle state. You can continue the conversation.",
        ['level' => 'info', 'type' => 'session_reset']
      );
    }

    $this->logger->info('Reset session @id, cancelled @count messages', [
      '@id' => $sessionId,
      '@count' => $resetCount,
    ]);

    return $resetCount;
  }

  /**
   * Gets the next sequence number for a session.
   *
   * @param \Drupal\flowdrop_session\Entity\FlowDropSessionInterface $session
   *   The session entity.
   *
   * @return int
   *   The next sequence number.
   */
  public function getNextSequenceNumber(FlowDropSessionInterface $session): int {
    $query = $this->messageStorage->getQuery()
      ->condition('session_id', $session->id())
      ->accessCheck(TRUE)
      ->sort('sequence_number', 'DESC')
      ->range(0, 1);

    $ids = $query->execute();
    if (empty($ids)) {
      return 1;
    }

    $lastMessage = $this->messageStorage->load(reset($ids));
    if ($lastMessage instanceof FlowDropSessionMessageInterface) {
      $lastSequence = $lastMessage->getSequenceNumber();
      return $lastSequence > 0 ? $lastSequence + 1 : 1;
    }

    return 1;
  }

  /**
   * Validates sequence ordering to ensure messages are processed in order.
   *
   * @param \Drupal\flowdrop_session\Entity\FlowDropSessionInterface $session
   *   The session entity.
   * @param int $sequence
   *   The sequence number to validate.
   *
   * @throws \RuntimeException
   *   If previous message is not complete.
   */
  protected function validateSequenceOrder(
    FlowDropSessionInterface $session,
    int $sequence,
  ): void {
    if ($sequence <= 1) {
      // First message, no validation needed.
      return;
    }

    // Get session ID and validate it exists.
    $sessionId = $session->id();
    if ($sessionId === NULL) {
      // Session has no ID, cannot validate sequence.
      return;
    }

    // Check if previous message is complete.
    $previousMessage = $this->getMessageBySequence((int) $sessionId, $sequence - 1);
    if ($previousMessage !== NULL && !$previousMessage->isTerminal()) {
      throw new \RuntimeException(
        sprintf(
          "Cannot process message: previous message (sequence %d) is not complete (status: %s)",
          $sequence - 1,
          $previousMessage->getStatus()
        )
      );
    }
  }

  /**
   * Gets a message by sequence number.
   *
   * @param int $session_id
   *   The session ID.
   * @param int $sequence
   *   The sequence number.
   *
   * @return \Drupal\flowdrop_session\Entity\FlowDropSessionMessageInterface|null
   *   The message entity or NULL if not found.
   */
  protected function getMessageBySequence(int $session_id, int $sequence): ?FlowDropSessionMessageInterface {
    $query = $this->messageStorage->getQuery()
      ->condition('session_id', $session_id)
      ->condition('sequence_number', $sequence)
      ->accessCheck(TRUE)
      ->range(0, 1);

    $ids = $query->execute();
    if (empty($ids)) {
      return NULL;
    }

    $message = $this->messageStorage->load(reset($ids));
    return $message instanceof FlowDropSessionMessageInterface ? $message : NULL;
  }

  /**
   * Queues a message for async processing.
   *
   * @param \Drupal\flowdrop_session\Entity\FlowDropSessionMessageInterface $message
   *   The message to queue.
   */
  protected function queueMessageProcessing(FlowDropSessionMessageInterface $message): void {
    $queue = $this->queueFactory->get('flowdrop_session_message');
    $queue->createItem([
      'message_id' => $message->id(),
      'session_id' => $message->getSessionId(),
      'sequence_number' => $message->getSequenceNumber(),
    ]);

    $this->logger->info('Queued message @id for async processing', [
      '@id' => $message->id(),
    ]);
  }

  /**
   * Finds the first ChatInput node in a workflow.
   *
   * @param \Drupal\flowdrop_workflow\FlowDropWorkflowInterface $workflow
   *   The workflow entity.
   *
   * @return array<string, mixed>|null
   *   The ChatInput node array or NULL if not found.
   */
  public function findChatInputNode(FlowDropWorkflowInterface $workflow): ?array {
    $nodes = $workflow->getNodes();
    $chatInputNodes = [];

    $patterns = ['chat_input.', 'text_input.'];

    foreach ($nodes as $node) {
      if (!is_array($node) || !isset($node['id'])) {
        continue;
      }

      // Check against node instance ID (e.g., "chat_input.1") not type ID.
      // The trailing dot in patterns prevents false matches
      // like "chat_input_custom.1".
      $nodeInstanceId = strtolower((string) $node['id']);

      foreach ($patterns as $pattern) {
        if (str_contains($nodeInstanceId, $pattern)) {
          $chatInputNodes[] = $node;
          break;
        }
      }
    }

    // Note: Session override would need to be passed separately if needed.
    // For now, we use the first match.
    // Return first match (warn if multiple found).
    if (!empty($chatInputNodes)) {
      if (count($chatInputNodes) > 1) {
        $this->logger->warning('Multiple ChatInput nodes found in workflow @id, using first', [
          '@id' => $workflow->id(),
        ]);
      }
      return $chatInputNodes[0];
    }

    return NULL;
  }

  /**
   * Formats a session entity for API response.
   *
   * @param \Drupal\flowdrop_session\Entity\FlowDropSessionInterface $session
   *   The session entity.
   *
   * @return array<string, mixed>
   *   Formatted session data.
   */
  public function formatSessionForApi(FlowDropSessionInterface $session): array {
    $owner = $session->getOwner();

    return [
      'id' => $session->uuid(),
      'workflowId' => $session->getWorkflowId(),
      'name' => $session->getName(),
      'status' => $session->getStatus(),
      'createdAt' => date('c', (int) $session->get('created')->value),
      'updatedAt' => date('c', (int) $session->getChangedTime()),
      'metadata' => $session->getMetadata(),
      'owner' => $owner !== NULL ? [
        'id' => $owner->id(),
        'name' => $owner->getDisplayName(),
      ] : NULL,
    ];
  }

  /**
   * Formats a message entity for API response.
   *
   * @param \Drupal\flowdrop_session\Entity\FlowDropSessionMessageInterface $message
   *   The message entity.
   *
   * @return array<string, mixed>
   *   Formatted message data.
   */
  public function formatMessageForApi(FlowDropSessionMessageInterface $message): array {
    $data = [
      'id' => $message->uuid(),
      'sessionId' => $message->getSession()?->uuid(),
      'role' => $message->getRole(),
      'content' => $message->getContent(),
      'timestamp' => date('c', $message->getTimestamp()),
      'status' => $message->getStatus(),
      'sequenceNumber' => $message->getSequenceNumber(),
      'nodeId' => $message->getNodeId(),
      'metadata' => $message->getMessageMetadata(),
    ];

    // Add optional linking fields.
    $parentMessageId = $message->getParentMessageId();
    if ($parentMessageId !== NULL) {
      $parentMessage = $this->messageStorage->load($parentMessageId);
      if ($parentMessage instanceof FlowDropSessionMessageInterface) {
        $data['parentMessageId'] = $parentMessage->uuid();
      }
    }

    $executionId = $message->getExecutionId();
    if ($executionId !== NULL) {
      $data['executionId'] = $executionId;
    }

    return $data;
  }

}
