<?php

declare(strict_types=1);

namespace Drupal\flowdrop_chat\Service;

use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Session\AccountProxyInterface;
use Drupal\flowdrop_memory\Service\MemoryManager;
use Drupal\flowdrop_session\Constants\MessageRole;
use Drupal\flowdrop_workflow\WorkflowDefinitionInterface;
use Drupal\flowdrop_workflow_executor\Enum\WorkflowExecutionMode;
use Drupal\flowdrop_workflow_executor\Service\WorkflowExecutionServiceInterface;
use Psr\Log\LoggerInterface;

/**
 * Orchestrates the full chat flow for FlowDrop.
 *
 * Loads/saves history via MemoryManager, builds system prompts,
 * calls the LLM, and returns the response.
 *
 * Supports two processing modes:
 * - builtin: built-in PHP pipeline (SystemPromptBuilder + ChatLlmClient).
 *   History is managed by this service.
 * - workflow: delegates all processing to a FlowDrop workflow.
 *   History is managed inside the workflow (MemoryRead / ConversationBuffer).
 */
class ChatService implements ChatServiceInterface {

  /**
   * The memory scope used for chat history storage (builtin mode only).
   */
  private const MEMORY_SCOPE = 'chat';

  /**
   * The memory key used for chat history storage.
   */
  private const MEMORY_KEY = 'history';

  /**
   * Processing mode: built-in PHP pipeline.
   */
  public const MODE_BUILTIN = 'builtin';

  /**
   * Processing mode: FlowDrop workflow.
   */
  public const MODE_WORKFLOW = 'workflow';

  /**
   * Constructs a new ChatService.
   *
   * @param \Drupal\flowdrop_chat\Service\ChatLlmClientInterface $llmClient
   *   The chat LLM client.
   * @param \Drupal\flowdrop_chat\Service\SystemPromptBuilderInterface $systemPromptBuilder
   *   The system prompt builder.
   * @param \Drupal\flowdrop_memory\Service\MemoryManager $memoryManager
   *   The memory manager for persisting chat history.
   * @param \Drupal\Core\Session\AccountProxyInterface $currentUser
   *   The current user.
   * @param \Drupal\Core\Config\ConfigFactoryInterface $configFactory
   *   The config factory.
   * @param \Drupal\flowdrop_workflow_executor\Service\WorkflowExecutionServiceInterface $workflowExecutionService
   *   The workflow execution service.
   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager
   *   The entity type manager.
   * @param \Psr\Log\LoggerInterface $logger
   *   The logger.
   */
  public function __construct(
    private readonly ChatLlmClientInterface $llmClient,
    private readonly SystemPromptBuilderInterface $systemPromptBuilder,
    private readonly MemoryManager $memoryManager,
    private readonly AccountProxyInterface $currentUser,
    private readonly ConfigFactoryInterface $configFactory,
    private readonly WorkflowExecutionServiceInterface $workflowExecutionService,
    private readonly EntityTypeManagerInterface $entityTypeManager,
    private readonly LoggerInterface $logger,
  ) {}

  /**
   * {@inheritdoc}
   */
  public function chat(string $workflowId, string $message, array|string|null $workflowState): array {
    $config = $this->configFactory->get('flowdrop_chat.settings');
    $processingMode = (string) ($config->get('processing_mode') ?: self::MODE_BUILTIN);
    $conversationKey = $this->getConversationKey($workflowId);

    $start = hrtime(TRUE);

    if ($processingMode === self::MODE_WORKFLOW) {
      // Workflow is fully self-contained — it fetches node types, builds the
      // system prompt, reads/writes history, and calls the LLM internally.
      $llmResponse = $this->chatViaWorkflow(
        $config->get('processing_workflow'),
        $message,
        $workflowState,
        $conversationKey,
      );
    }
    else {
      // Builtin: load history, process, save history.
      $maxHistoryLength = (int) $config->get('max_history_length');
      $memoryBackend = (string) ($config->get('memory_backend') ?: 'entity');

      $history = $this->loadHistory($workflowId, $memoryBackend);
      $llmResponse = $this->chatViaBuiltin($message, $workflowState, $history);

      $history[] = ['role' => MessageRole::USER, 'content' => $message];
      $history[] = ['role' => MessageRole::ASSISTANT, 'content' => $llmResponse];

      if (count($history) > $maxHistoryLength) {
        $history = array_slice($history, -$maxHistoryLength);
      }

      $this->memoryManager->set(
        self::MEMORY_SCOPE,
        $conversationKey,
        self::MEMORY_KEY,
        $history,
        NULL,
        $memoryBackend,
      );
    }

    $elapsedMs = (int) round((hrtime(TRUE) - $start) / 1_000_000);
    $this->logger->info('Chat processing [{mode}] completed in {ms}ms for workflow {workflow_id}.', [
      'mode' => $processingMode,
      'ms' => $elapsedMs,
      'workflow_id' => $workflowId,
    ]);

    return [
      'content' => $llmResponse,
      'conversationId' => $workflowId,
    ];
  }

  /**
   * {@inheritdoc}
   */
  public function getHistory(string $workflowId): array {
    $config = $this->configFactory->get('flowdrop_chat.settings');
    $processingMode = (string) ($config->get('processing_mode') ?: self::MODE_BUILTIN);

    if ($processingMode === self::MODE_WORKFLOW) {
      // History is managed inside the workflow's own memory nodes.
      return [];
    }

    $memoryBackend = (string) ($config->get('memory_backend') ?: 'entity');
    return $this->loadHistory($workflowId, $memoryBackend);
  }

  /**
   * {@inheritdoc}
   */
  public function clearHistory(string $workflowId): void {
    $config = $this->configFactory->get('flowdrop_chat.settings');
    $processingMode = (string) ($config->get('processing_mode') ?: self::MODE_BUILTIN);

    if ($processingMode === self::MODE_WORKFLOW) {
      // History is owned by the workflow; nothing to clear here.
      return;
    }

    $memoryBackend = (string) ($config->get('memory_backend') ?: 'entity');
    $conversationKey = $this->getConversationKey($workflowId);

    $this->memoryManager->delete(
      self::MEMORY_SCOPE,
      $conversationKey,
      self::MEMORY_KEY,
      $memoryBackend,
    );
  }

  /**
   * Processes a message using the built-in PHP pipeline.
   *
   * @param string $message
   *   The user message.
   * @param array|string|null $workflowState
   *   The current workflow state.
   * @param array $history
   *   The chat history.
   *
   * @return string
   *   The LLM response.
   */
  private function chatViaBuiltin(string $message, array|string|null $workflowState, array $history): string {
    $systemPrompt = $this->systemPromptBuilder->build($workflowState, $history);
    return $this->llmClient->chat($systemPrompt, $message, ['flowdrop_chat']);
  }

  /**
   * Processes a message by executing a FlowDrop workflow.
   *
   * The workflow receives initial data keyed by node ID (the runtime injects
   * per-node data via initialData[$nodeId]). Expected node IDs:
   * - text_input.1: receives ['data' => $message]
   * - text_input.2: receives ['data' => $workflowState]
   * - text_input.3: receives ['data' => $conversationKey]
   *
   * The workflow is expected to handle all processing internally:
   * fetching node types (GetNodeTypes), building the system prompt
   * (BuildSystemPrompt), reading/writing history (MemoryRead/Buffer),
   * and calling the LLM (Chat node).
   *
   * The workflow must output a 'content' key with the assistant response.
   * If no 'content' key is present, the first string output value is used.
   *
   * @param string|null $processingWorkflowId
   *   The ID of the FlowDrop workflow to execute.
   * @param string $message
   *   The user message.
   * @param array|string|null $workflowState
   *   The current workflow state.
   * @param string $conversationKey
   *   The memory key scoping history to this workflow + user pair.
   *
   * @return string
   *   The assistant response extracted from workflow outputs.
   *
   * @throws \RuntimeException
   *   When the workflow is not configured, not found, or execution fails.
   */
  private function chatViaWorkflow(
    string|null $processingWorkflowId,
    string $message,
    array|string|null $workflowState,
    string $conversationKey,
  ): string {
    if (empty($processingWorkflowId)) {
      throw new \RuntimeException('No processing workflow configured. Set a workflow ID in FlowDrop Chat settings.');
    }

    $workflow = $this->entityTypeManager
      ->getStorage('flowdrop_workflow')
      ->load($processingWorkflowId);

    if (!$workflow instanceof WorkflowDefinitionInterface) {
      throw new \RuntimeException(sprintf('Processing workflow "%s" not found.', $processingWorkflowId));
    }

    // The runtime injects per-node initial data keyed by node ID.
    // The flowdrop_chat_processor workflow expects these specific node IDs.
    $result = $this->workflowExecutionService->execute(
      $workflow,
      [
        'text_input.1' => ['data' => $message],
        'text_input.2' => ['data' => $workflowState],
        'text_input.3' => ['data' => $conversationKey],
      ],
      WorkflowExecutionMode::Job,
    );

    if (!$result->isSuccess()) {
      throw new \RuntimeException(sprintf('Workflow execution failed: %s', $result->getErrorMessage()));
    }

    $outputs = $result->getOutputs();

    if (isset($outputs['content']) && is_string($outputs['content'])) {
      return $outputs['content'];
    }

    // Fall back to first string value in outputs.
    foreach ($outputs as $value) {
      if (is_string($value) && $value !== '') {
        return $value;
      }
    }

    throw new \RuntimeException('Workflow did not return a usable response. Ensure the workflow has an output node with key "content".');
  }

  /**
   * Builds the conversation key from workflow ID and current user ID.
   *
   * @param string $workflowId
   *   The workflow ID.
   *
   * @return string
   *   The conversation key in format '{workflowId}:{userId}'.
   */
  private function getConversationKey(string $workflowId): string {
    return $workflowId . ':' . $this->currentUser->id();
  }

  /**
   * Loads chat history from MemoryManager (builtin mode only).
   *
   * @param string $workflowId
   *   The workflow ID.
   * @param string $memoryBackend
   *   The memory backend ID.
   *
   * @return array<int, array{role: string, content: string}>
   *   The chat history.
   */
  private function loadHistory(string $workflowId, string $memoryBackend): array {
    $conversationKey = $this->getConversationKey($workflowId);

    /** @var array<int, array{role: string, content: string}> $history */
    $history = $this->memoryManager->get(
      self::MEMORY_SCOPE,
      $conversationKey,
      self::MEMORY_KEY,
      [],
      $memoryBackend,
    );

    return $history;
  }

}
