<?php

declare(strict_types=1);

namespace Drupal\flowdrop_session\Service;

use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Logger\LoggerChannelFactoryInterface;
use Drupal\flowdrop_orchestration\DTO\Orchestrator\OrchestrationRequest;
use Drupal\flowdrop_orchestration\DTO\Orchestrator\OrchestrationResponse;
use Drupal\flowdrop_orchestration\Plugin\Orchestrator\OrchestratorPluginInterface;
use Drupal\flowdrop_orchestration\Service\Orchestrator\OrchestratorHelperInterface;
use Drupal\flowdrop_session\Constants\MessageStatus;
use Drupal\flowdrop_session\Constants\SessionStatus;
use Drupal\flowdrop_session\Entity\FlowDropSessionInterface;
use Drupal\flowdrop_session\Entity\FlowDropSessionMessageInterface;
use Drupal\flowdrop_workflow\FlowDropWorkflowInterface;
use Psr\Log\LoggerInterface;

/**
 * Handles workflow execution for session messages.
 *
 * This service contains the execution logic extracted from SessionService:
 * - Processing user messages by executing workflows
 * - Building orchestration requests and handling responses
 * - Extracting message content from workflow results
 * - Handling interrupted executions (human-in-loop)
 *
 * @internal This class is not part of the public API and may change without notice.
 */
class SessionExecutionService implements MessageProcessorInterface {

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

  /**
   * Constructs a new SessionExecutionService.
   *
   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager
   *   The entity type manager.
   * @param \Drupal\Core\Logger\LoggerChannelFactoryInterface $loggerFactory
   *   The logger factory.
   * @param \Drupal\flowdrop_orchestration\Service\Orchestrator\OrchestratorHelperInterface $orchestratorHelper
   *   The orchestrator helper service.
   * @param \Drupal\flowdrop_session\Service\SessionService $sessionService
   *   The session service for message/session CRUD operations.
   */
  public function __construct(
    protected readonly EntityTypeManagerInterface $entityTypeManager,
    LoggerChannelFactoryInterface $loggerFactory,
    protected readonly OrchestratorHelperInterface $orchestratorHelper,
    protected readonly SessionService $sessionService,
  ) {
    $this->logger = $loggerFactory->get('flowdrop_session');
  }

  /**
   * {@inheritdoc}
   */
  public function processMessage(FlowDropSessionMessageInterface $message): void {
    $session = $message->getSession();
    if ($session === NULL) {
      throw new \RuntimeException('Message has no associated session');
    }

    $workflow = $session->getWorkflow();
    if ($workflow === NULL) {
      throw new \RuntimeException('Session has no associated workflow');
    }

    // Update status to processing.
    $message->setStatus(MessageStatus::PROCESSING)->save();
    $session->setStatus(SessionStatus::RUNNING)->save();

    try {
      // Find ChatInput node.
      $chatInputNode = $this->sessionService->findChatInputNode($workflow);

      // Build initialData (same pattern as triggers).
      $initialData = $this->buildInitialData($message, $chatInputNode, $session);

      // Get orchestrator settings (session overrides workflow).
      $orchestratorSettings = $this->getOrchestratorSettings($session, $workflow);
      $orchestrator = $this->getOrchestrator($orchestratorSettings);

      // Build orchestration request (same as triggers).
      $request = new OrchestrationRequest(
        workflowId: (string) $workflow->id(),
        pipelineId: "playground_session_{$session->id()}_" . time(),
        workflow: $this->workflowToArray($workflow),
        initialData: $initialData,
        options: $this->buildOrchestrationOptions($orchestratorSettings, $session, $message),
      );

      // Execute (direct return, like triggers).
      $response = $orchestrator->orchestrate($request);

      // Store execution ID in message metadata.
      $executionId = $response->getMetadata()['execution_id'] ?? NULL;
      if ($executionId !== NULL) {
        $message->setExecutionId($executionId);
      }

      // Process results FIRST - this creates chat messages for any chat_output
      // nodes that executed before an interrupt. Ensures users see messages
      // from nodes that ran even if the workflow is interrupted.
      $this->processExecutionResults($message, $response);

      // Check for interrupted status (human-in-loop).
      if ($response->getStatus() === 'interrupted') {
        $this->handleInterruptedExecution($message, $session, $response);
        return;
      }

      // Update message and session status.
      $message->setStatus(MessageStatus::COMPLETED)->save();
      $session->setStatus(SessionStatus::IDLE)->save();

      $this->logger->info('Message @id processed successfully', [
        '@id' => $message->id(),
      ]);
    }
    catch (\Exception $e) {
      $message->setStatus(MessageStatus::FAILED)->save();
      $session->setStatus(SessionStatus::IDLE)->save();
      $this->createErrorMessage($session, $message, $e);
      throw $e;
    }
  }

  /**
   * Finds the chat input node and its primary input parameter.
   *
   * Looks for nodes that act as chat inputs (typically nodes with specific
   * types like 'chat_input', 'text_input', etc.) and determines the correct
   * parameter name to use for injecting the session input.
   *
   * @param \Drupal\flowdrop_workflow\FlowDropWorkflowInterface $workflow
   *   The workflow entity.
   *
   * @return array{node_id: string, parameter_name: string,
   *   node_type_id: string}|null
   *   Array with node_id, parameter_name, and node_type_id, or NULL if not
   *   found.
   */
  protected function findChatInputNodeWithParameter(FlowDropWorkflowInterface $workflow): ?array {
    $nodes = $workflow->getNodes();

    // Patterns that indicate a chat/text input node.
    $input_patterns = [
      'chat_input',
      'chatinput',
      'text_input',
      'textinput',
      'user_input',
      'message_input',
      'prompt_input',
    ];

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

      $node_id = $node['id'];
      $node_type = strtolower((string) ($node['type'] ?? ''));
      $metadata = $node['data']['metadata'] ?? [];
      $node_type_id = (string) ($metadata['id'] ?? '');
      $node_type_id_lower = strtolower($node_type_id);
      $node_name = strtolower((string) ($metadata['name'] ?? ''));
      $category = strtolower((string) ($metadata['category'] ?? ''));

      $is_input_node = FALSE;

      // Check node type ID first (most reliable - e.g., 'text_input').
      foreach ($input_patterns as $pattern) {
        if (str_contains($node_type_id_lower, $pattern)) {
          $is_input_node = TRUE;
          break;
        }
      }

      // Check node type and name.
      if (!$is_input_node) {
        foreach ($input_patterns as $pattern) {
          if (str_contains($node_type, $pattern) || str_contains($node_name, $pattern)) {
            $is_input_node = TRUE;
            break;
          }
        }
      }

      // Check category for input nodes.
      if (!$is_input_node && ($category === 'inputs' || $category === 'input')) {
        foreach ($input_patterns as $pattern) {
          if (str_contains($node_name, $pattern) || str_contains($node_type_id_lower, $pattern)) {
            $is_input_node = TRUE;
            break;
          }
        }
      }

      if ($is_input_node) {
        // Determine the primary input parameter for this node type.
        $parameter_name = $this->getPrimaryInputParameter($node_type_id, $node);

        $this->logger->debug('Found input node @id (@type_id), using parameter "@param"', [
          '@id' => $node_id,
          '@type_id' => $node_type_id,
          '@param' => $parameter_name,
        ]);

        return [
          'node_id' => $node_id,
          'parameter_name' => $parameter_name,
          'node_type_id' => $node_type_id,
        ];
      }
    }

    $this->logger->warning('No chat/text input node found in workflow @id', [
      '@id' => $workflow->id(),
    ]);

    return NULL;
  }

  /**
   * Gets the primary input parameter name for a node type.
   *
   * This method queries the node type entity to find the first connectable
   * parameter, providing a standardized way to inject initial data.
   *
   * @param string $node_type_id
   *   The node type entity ID.
   * @param array<string, mixed> $node
   *   The workflow node data (for fallback extraction).
   *
   * @return string
   *   The parameter name to use for initial input.
   */
  protected function getPrimaryInputParameter(string $node_type_id, array $node): string {
    // Try to load the node type entity.
    if (!empty($node_type_id)) {
      try {
        $node_type_storage = $this->entityTypeManager->getStorage('flowdrop_node_type');
        $node_type = $node_type_storage->load($node_type_id);

        if ($node_type !== NULL && method_exists($node_type, 'getParameters')) {
          $parameters = $node_type->getParameters();

          // Find the first connectable parameter.
          foreach ($parameters as $param_name => $param_config) {
            if (!empty($param_config['connectable'])) {
              $this->logger->debug('Found connectable parameter "@param" for node type @type', [
                '@param' => $param_name,
                '@type' => $node_type_id,
              ]);
              return $param_name;
            }
          }

          // If no connectable parameter found, look for common input
          // parameter names.
          $common_input_params = ['data', 'value', 'input', 'text', 'message', 'content'];
          foreach ($common_input_params as $param_name) {
            if (isset($parameters[$param_name])) {
              return $param_name;
            }
          }
        }
      }
      catch (\Exception $e) {
        $this->logger->warning('Failed to load node type @type: @message', [
          '@type' => $node_type_id,
          '@message' => $e->getMessage(),
        ]);
      }
    }

    // Fallback: try to extract from node's input schema in metadata.
    $input_schema = $node['data']['metadata']['inputSchema']['properties'] ?? [];
    if (!empty($input_schema)) {
      // Return the first input port name.
      $first_key = array_key_first($input_schema);
      if ($first_key !== NULL && $first_key !== 'trigger') {
        return $first_key;
      }
    }

    // Final fallback: use 'data' as the default parameter name.
    // This matches the TextInput node's expected parameter.
    return 'data';
  }

  /**
   * Builds initial data for workflow execution.
   *
   * @param \Drupal\flowdrop_session\Entity\FlowDropSessionMessageInterface $message
   *   The user message.
   * @param array<string, mixed>|null $chatInputNode
   *   The ChatInput node or NULL.
   * @param \Drupal\flowdrop_session\Entity\FlowDropSessionInterface $session
   *   The session entity.
   *
   * @return array<string, mixed>
   *   The initial data array.
   */
  protected function buildInitialData(
    FlowDropSessionMessageInterface $message,
    ?array $chatInputNode,
    FlowDropSessionInterface $session,
  ): array {
    $initialData = [];

    // Get message metadata (may contain additional inputs).
    $metadata = $message->getMessageMetadata();
    $additionalInputs = $metadata['inputs'] ?? [];

    if ($chatInputNode !== NULL) {
      $nodeId = $chatInputNode['id'] ?? NULL;
      if ($nodeId !== NULL) {
        // Build ChatInput node data.
        $nodeData = [
          'message' => $message->getContent(),
        ];

        // Add optional conversation history.
        $history = $this->getMessageHistoryForChatInput($session);
        if (!empty($history)) {
          $nodeData['history'] = $history;
        }

        // Add session and message IDs for tracking.
        // Cast to integers to satisfy strict type validation in Resolver.
        $sessionIdValue = $session->id();
        $messageIdValue = $message->id();
        $nodeData['sessionId'] = $sessionIdValue !== NULL ? (int) $sessionIdValue : NULL;
        $nodeData['messageId'] = $messageIdValue !== NULL ? (int) $messageIdValue : NULL;

        $initialData[$nodeId] = $nodeData;
      }
    }

    // Merge additional inputs.
    $initialData = array_merge($initialData, $additionalInputs);

    return $initialData;
  }

  /**
   * Gets orchestrator settings (session overrides workflow).
   *
   * @param \Drupal\flowdrop_session\Entity\FlowDropSessionInterface $session
   *   The session entity.
   * @param \Drupal\flowdrop_workflow\FlowDropWorkflowInterface $workflow
   *   The workflow entity.
   *
   * @return array<string, mixed>
   *   The orchestrator settings.
   */
  protected function getOrchestratorSettings(
    FlowDropSessionInterface $session,
    FlowDropWorkflowInterface $workflow,
  ): array {
    // Session settings override workflow defaults.
    $sessionSettings = $session->getOrchestratorSettings();
    if (!empty($sessionSettings)) {
      return $sessionSettings;
    }

    // Fall back to workflow metadata if available.
    $workflowMetadata = $workflow->getMetadata();
    return $workflowMetadata['orchestrator_settings'] ?? [];
  }

  /**
   * Gets the orchestrator instance from settings.
   *
   * @param array<string, mixed> $settings
   *   The orchestrator settings.
   *
   * @return \Drupal\flowdrop_orchestration\Plugin\Orchestrator\OrchestratorPluginInterface
   *   The orchestrator instance.
   */
  protected function getOrchestrator(array $settings): OrchestratorPluginInterface {
    // Use StateGraph as default for session (interactive chat workflows).
    return $this->orchestratorHelper->getInstanceFromSettings(
      $settings,
      OrchestratorHelperInterface::STATEGRAPH
    );
  }

  /**
   * Builds orchestration options from settings.
   *
   * @param array<string, mixed> $orchestratorSettings
   *   The orchestrator settings.
   * @param \Drupal\flowdrop_session\Entity\FlowDropSessionInterface $session
   *   The session entity.
   * @param \Drupal\flowdrop_session\Entity\FlowDropSessionMessageInterface $message
   *   The message entity.
   *
   * @return array<string, mixed>
   *   The orchestration options.
   */
  protected function buildOrchestrationOptions(
    array $orchestratorSettings,
    FlowDropSessionInterface $session,
    FlowDropSessionMessageInterface $message,
  ): array {
    $options = [];

    // Add StateGraph-specific options if orchestrator is stategraph.
    $orchestratorType = $orchestratorSettings['type'] ?? OrchestratorHelperInterface::STATEGRAPH;
    if ($this->orchestratorHelper->isStateGraph($orchestratorType)) {
      $options['stategraph'] = [
        'thread_id' => "playground_session_{$session->id()}",
        'create_checkpoints' => TRUE,
        'max_iterations' => $orchestratorSettings['max_iterations'] ?? 100,
      ];
    }

    // Add session-specific metadata.
    $options['playground'] = [
      'session_id' => $session->id(),
      'message_id' => $message->id(),
      'sequence_number' => $message->getSequenceNumber(),
    ];

    return $options;
  }

  /**
   * Converts workflow entity to array format.
   *
   * @param \Drupal\flowdrop_workflow\FlowDropWorkflowInterface $workflow
   *   The workflow entity.
   *
   * @return array<string, mixed>
   *   The workflow array.
   */
  protected function workflowToArray(FlowDropWorkflowInterface $workflow): array {
    return [
      'id' => $workflow->id(),
      'label' => $workflow->label(),
      'description' => $workflow->getDescription(),
      'nodes' => $workflow->getNodes(),
      'edges' => $workflow->getEdges(),
      'metadata' => $workflow->getMetadata(),
    ];
  }

  /**
   * Gets message history for ChatInput nodes with caching.
   *
   * @param \Drupal\flowdrop_session\Entity\FlowDropSessionInterface $session
   *   The session entity.
   * @param int $limit
   *   Maximum number of messages to return.
   *
   * @return array<int, array<string, mixed>>
   *   Array of message history items.
   */
  protected function getMessageHistoryForChatInput(
    FlowDropSessionInterface $session,
    int $limit = 10,
  ): array {
    // Check session metadata cache.
    $cache = $session->getMetadata()['message_history'] ?? NULL;
    if ($cache !== NULL &&
        (time() - ($cache['last_updated'] ?? 0)) < 300 &&
        count($cache['messages'] ?? []) >= $limit) {
      return $cache['messages'];
    }

    // Fetch from database.
    $sessionId = $session->id();
    if ($sessionId === NULL) {
      return [];
    }
    $messages = $this->sessionService->getMessages((int) $sessionId, NULL, $limit);
    $history = [];

    foreach ($messages as $message) {
      if (in_array($message->getRole(), ['user', 'assistant'], TRUE)) {
        $history[] = [
          'role' => $message->getRole(),
          'content' => $message->getContent(),
          'timestamp' => $message->getTimestamp(),
        ];
      }
    }

    // Update cache.
    $metadata = $session->getMetadata();
    $metadata['message_history'] = [
      'messages' => $history,
      'last_updated' => time(),
      'message_count' => count($history),
    ];
    $session->setMetadata($metadata)->save();

    return $history;
  }

  /**
   * Extracts message content from node output.
   *
   * Uses a simple, predictable key-based approach:
   * 1. Check standard output keys in priority order (message, text, etc.)
   * 2. Fall back to first non-metadata key in the output
   * 3. JSON encoding as last resort.
   *
   * @param mixed $output
   *   The node output data.
   * @param string|null $node_type_id
   *   Optional node type ID (reserved for future use).
   *
   * @return string|null
   *   The extracted message content, or NULL if not found.
   */
  protected function extractMessageContent(mixed $output, ?string $node_type_id = NULL): ?string {
    if (is_string($output)) {
      return $output;
    }

    if (!is_array($output)) {
      return NULL;
    }

    // Standard output keys in priority order.
    // Use explicit key names for predictable behavior rather than
    // relying on node type configuration order.
    $content_keys = [
    // ChatInput, ChatOutput use this.
      "message",
    // TextInput, TextOutput use this.
      "text",
    // General output key.
      "output",
    // General content key.
      "content",
    // General value key.
      "value",
    // General result key.
      "result",
    // AI/chat responses.
      "response",
    ];

    foreach ($content_keys as $key) {
      if (isset($output[$key])) {
        return $this->valueToString($output[$key]);
      }
    }

    // If no standard key found, try the first non-metadata key.
    foreach ($output as $key => $value) {
      // Skip internal/metadata keys.
      if (str_starts_with($key, "_") || $key === "success" || $key === "error" || $key === "format") {
        continue;
      }
      return $this->valueToString($value);
    }

    // Fallback: encode entire output as JSON.
    $encoded = json_encode($output, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
    return $encoded !== FALSE ? $encoded : NULL;
  }

  /**
   * Converts a value to a string representation.
   *
   * @param mixed $value
   *   The value to convert.
   *
   * @return string
   *   The string representation.
   */
  protected function valueToString(mixed $value): string {
    if (is_string($value)) {
      return $value;
    }

    if (is_numeric($value)) {
      return (string) $value;
    }

    if (is_bool($value)) {
      return $value ? 'true' : 'false';
    }

    if (is_array($value)) {
      // If it's a simple array of strings, join them.
      if (array_is_list($value) && !empty($value) && is_string($value[0])) {
        return implode("\n", array_map('strval', $value));
      }
      return json_encode($value, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
    }

    return (string) $value;
  }

  /**
   * Gets the primary output parameter name for a node type.
   *
   * @param string $node_type_id
   *   The node type entity ID.
   *
   * @return string|null
   *   The primary output parameter name, or NULL if not found.
   */
  protected function getPrimaryOutputParameter(string $node_type_id): ?string {
    if (empty($node_type_id)) {
      return NULL;
    }

    try {
      $node_type_storage = $this->entityTypeManager->getStorage('flowdrop_node_type');
      $node_type = $node_type_storage->load($node_type_id);

      if ($node_type !== NULL && method_exists($node_type, 'getOutputs')) {
        $outputs = $node_type->getOutputs();

        // First, look for an output marked as primary.
        foreach ($outputs as $output_name => $output_config) {
          if (!empty($output_config['primary'])) {
            return $output_name;
          }
        }

        // Fallback: look for common content output names (message, text, etc.)
        // to provide better defaults for chat/text output nodes.
        $contentOutputNames = ['message', 'text', 'output', 'content', 'result', 'response'];
        foreach ($contentOutputNames as $contentName) {
          if (isset($outputs[$contentName])) {
            $config = $outputs[$contentName];
            if (!isset($config['exposed']) || $config['exposed']) {
              return $contentName;
            }
          }
        }

        // Final fallback: find the first exposed output.
        foreach ($outputs as $output_name => $output_config) {
          if (!isset($output_config['exposed']) || $output_config['exposed']) {
            return $output_name;
          }
        }
      }
    }
    catch (\Exception $e) {
      // Silently fail - we'll use fallback logic.
    }

    return NULL;
  }

  /**
   * Creates an error message for failed execution.
   *
   * @param \Drupal\flowdrop_session\Entity\FlowDropSessionInterface $session
   *   The session entity.
   * @param \Drupal\flowdrop_session\Entity\FlowDropSessionMessageInterface $userMessage
   *   The user message that failed.
   * @param \Exception $exception
   *   The exception that occurred.
   */
  protected function createErrorMessage(
    FlowDropSessionInterface $session,
    FlowDropSessionMessageInterface $userMessage,
    \Exception $exception,
  ): void {
    $this->sessionService->addSystemMessage($session, "Execution failed: {$exception->getMessage()}", [
      'level' => 'error',
      'error_type' => get_class($exception),
      'user_message_id' => $userMessage->id(),
    ]);
  }

  /**
   * Handles interrupted workflow execution (human-in-loop).
   *
   * When a workflow is interrupted for user input, this method:
   * - Creates a system message with the interrupt details.
   * - Updates session status to 'awaiting_input'.
   * - Stores the pending interrupt ID in session metadata.
   *
   * @param \Drupal\flowdrop_session\Entity\FlowDropSessionMessageInterface $message
   *   The user message that triggered execution.
   * @param \Drupal\flowdrop_session\Entity\FlowDropSessionInterface $session
   *   The session entity.
   * @param \Drupal\flowdrop_orchestration\DTO\Orchestrator\OrchestrationResponse $response
   *   The orchestration response.
   */
  protected function handleInterruptedExecution(
    FlowDropSessionMessageInterface $message,
    FlowDropSessionInterface $session,
    OrchestrationResponse $response,
  ): void {
    $metadata = $response->getMetadata();
    $interruptData = $metadata['interrupt'] ?? [];
    $interrupt = $interruptData['interrupt'] ?? [];

    $interruptId = $interrupt['id'] ?? NULL;
    $interruptType = $interrupt['type'] ?? 'unknown';
    $interruptMessage = $interrupt['message'] ?? 'User input required';
    $interruptSchema = $interrupt['schema'] ?? NULL;
    $interruptOptions = $interrupt['options'] ?? NULL;
    $interruptDefaultValue = $interrupt['defaultValue'] ?? NULL;

    // Build message metadata with flat keys for the frontend library.
    // The library's extractInterruptMetadata() expects these as top-level
    // fields rather than nested inside schema/options.
    $messageMetadata = [
      'type' => 'interrupt_request',
      'interrupt_id' => $interruptId,
      'interrupt_type' => $interruptType,
      'schema' => $interruptSchema,
      'options' => $interruptOptions,
      'default_value' => $interruptDefaultValue,
      'node_id' => $interrupt['nodeId'] ?? NULL,
      'execution_id' => $response->getExecutionId(),
      'allow_cancel' => TRUE,
    ];

    // Extract type-specific fields from schema/options into flat keys.
    if ($interruptType === 'confirmation' && is_array($interruptOptions)) {
      foreach ($interruptOptions as $option) {
        if (isset($option['value'], $option['label'])) {
          if ($option['value'] === 'true' || $option['value'] === TRUE) {
            $messageMetadata['confirm_label'] = $option['label'];
          }
          elseif ($option['value'] === 'false' || $option['value'] === FALSE) {
            $messageMetadata['cancel_label'] = $option['label'];
          }
        }
      }
    }
    if (is_array($interruptSchema)) {
      if (isset($interruptSchema['placeholder'])) {
        $messageMetadata['placeholder'] = $interruptSchema['placeholder'];
      }
      if (isset($interruptSchema['multiline'])) {
        $messageMetadata['multiline'] = (bool) $interruptSchema['multiline'];
      }
      if (isset($interruptSchema['minLength'])) {
        $messageMetadata['min_length'] = (int) $interruptSchema['minLength'];
      }
      if (isset($interruptSchema['maxLength'])) {
        $messageMetadata['max_length'] = (int) $interruptSchema['maxLength'];
      }
    }

    // Create system message with interrupt info.
    $this->sessionService->addSystemMessage($session, $interruptMessage, $messageMetadata);

    // Update session status to awaiting_input.
    $session->setStatus(SessionStatus::AWAITING_INPUT);
    $sessionMetadata = $session->getMetadata();
    $sessionMetadata['pending_interrupt_id'] = $interruptId;
    $sessionMetadata['pending_interrupt_type'] = $interruptType;
    $sessionMetadata['pending_execution_id'] = $response->getExecutionId();
    $session->setMetadata($sessionMetadata);
    $session->save();

    // Update message status to awaiting_input.
    $message->setStatus(MessageStatus::AWAITING_INPUT)->save();

    $this->logger->info('Workflow interrupted for session @session, interrupt @interrupt', [
      '@session' => $session->id(),
      '@interrupt' => $interruptId,
    ]);
  }

  /**
   * Processes execution results and creates assistant messages.
   *
   * This method handles both single-execution and loop iteration results.
   * Results can have keys in two formats:
   * - Simple: "node_id" (e.g., "chat_output.1")
   * - Iteration: "node_id:n" (e.g., "chat_output.1:0", "chat_output.1:1")
   *
   * For loop iterations, a separate message is created for each iteration.
   *
   * @param \Drupal\flowdrop_session\Entity\FlowDropSessionMessageInterface $userMessage
   *   The user message that triggered execution.
   * @param \Drupal\flowdrop_orchestration\DTO\Orchestrator\OrchestrationResponse $response
   *   The orchestration response.
   */
  protected function processExecutionResults(
    FlowDropSessionMessageInterface $userMessage,
    OrchestrationResponse $response,
  ): void {
    $session = $userMessage->getSession();
    if ($session === NULL) {
      return;
    }

    $workflow = $session->getWorkflow();
    if ($workflow === NULL) {
      return;
    }

    $results = $response->getResults();
    $nodes = $workflow->getNodes();
    $messagesCreated = FALSE;

    // Patterns that indicate a chat/text output node.
    $outputPatterns = [
      'chat_output',
      'chatoutput',
      'text_output',
      'textoutput',
      'message_output',
      'chat_message',
      'response_output',
    ];

    // Build a map of output node IDs and their types.
    $outputNodes = [];
    foreach ($nodes as $node) {
      if (!is_array($node) || !isset($node['id'])) {
        continue;
      }

      $nodeId = $node['id'];
      $metadata = $node['data']['metadata'] ?? [];
      $nodeTypeId = strtolower((string) ($metadata['id'] ?? ''));

      // Check if this is an output node.
      foreach ($outputPatterns as $pattern) {
        if (str_contains($nodeTypeId, $pattern)) {
          $outputNodes[$nodeId] = $nodeTypeId;
          break;
        }
      }
    }

    // Process results - handle both simple keys and iteration keys.
    // Results are sorted to ensure iteration order is preserved.
    $resultKeys = array_keys($results);
    sort($resultKeys);

    foreach ($resultKeys as $resultKey) {
      // Parse the result key to extract node ID and optional iteration.
      $parsedKey = $this->parseResultKey($resultKey);
      $nodeId = $parsedKey['nodeId'];
      $iteration = $parsedKey['iteration'];

      // Check if this result is for an output node.
      if (!isset($outputNodes[$nodeId])) {
        continue;
      }

      $nodeTypeId = $outputNodes[$nodeId];
      $output = $results[$resultKey];
      $content = $this->extractMessageContent($output, $nodeTypeId);

      if ($content !== NULL && $content !== '') {
        // Get next sequence number for this assistant message.
        $sequenceNumber = $this->sessionService->getNextSequenceNumber($session);

        // Include iteration info in metadata if present.
        $messageMetadata = [
          'source' => 'workflow_execution',
          'node_type' => $nodeTypeId,
          'execution_id' => $userMessage->getExecutionId(),
          'result_key' => $resultKey,
          'sequence_number' => $sequenceNumber,
        ];

        if ($iteration !== NULL) {
          $messageMetadata['loop_iteration'] = $iteration;
        }

        $assistantMessage = $this->sessionService->addAssistantMessage(
          $session,
          $content,
          $nodeId,
          $messageMetadata
        );

        // Link assistant message to user message.
        $userMessageId = $userMessage->id();
        if ($userMessageId !== NULL) {
          $assistantMessage->setParentMessageId((int) $userMessageId);
        }
        $assistantMessage->setExecutionId($userMessage->getExecutionId());
        $assistantMessage->setStatus(MessageStatus::COMPLETED);
        $assistantMessage->save();

        $messagesCreated = TRUE;
      }
    }

    // If no output messages were created, add a system message.
    if (!$messagesCreated) {
      if (empty($results)) {
        $this->sessionService->addSystemMessage($session, 'Workflow execution completed with no output.', [
          'level' => 'info',
        ]);
      }
    }
  }

  /**
   * Parses a result key to extract node ID and optional iteration number.
   *
   * Result keys can be in two formats:
   * - Simple: "node_id" (e.g., "chat_output.1")
   * - Iteration: "node_id:iteration" (e.g., "chat_output.1:0")
   *
   * @param string $resultKey
   *   The result key to parse.
   *
   * @return array{nodeId: string, iteration: int|null}
   *   Array with 'nodeId' and 'iteration' (null if no iteration suffix).
   */
  protected function parseResultKey(string $resultKey): array {
    // Check for iteration suffix (colon separator).
    if (str_contains($resultKey, ':')) {
      $parts = explode(':', $resultKey, 2);
      $nodeId = $parts[0];
      $iteration = is_numeric($parts[1]) ? (int) $parts[1] : NULL;

      return [
        'nodeId' => $nodeId,
        'iteration' => $iteration,
      ];
    }

    // No iteration suffix - simple node ID.
    return [
      'nodeId' => $resultKey,
      'iteration' => NULL,
    ];
  }

}
