<?php

declare(strict_types=1);

namespace Drupal\ai\Symfony;

use Drupal\ai\Dto\TokenUsageDto;
use Drupal\ai\OperationType\Chat\ChatInput;
use Drupal\ai\OperationType\Chat\ChatMessage;
use Drupal\ai\OperationType\Chat\ChatOutput;
use Drupal\ai\OperationType\Chat\StreamedChatMessageIterator;
use Drupal\ai\OperationType\Chat\Tools\ToolsFunctionOutput;
use Drupal\ai\OperationType\Chat\Tools\ToolsInputInterface;
use Symfony\AI\Platform\Message\Content\Image;
use Symfony\AI\Platform\Message\Content\Text;
use Symfony\AI\Platform\Message\Message;
use Symfony\AI\Platform\Message\MessageBag;
use Symfony\AI\Platform\Result\DeferredResult;
use Symfony\AI\Platform\Result\ToolCall;
use Symfony\AI\Platform\Result\ToolCallResult;
use Symfony\AI\Platform\Tool\ExecutionReference;
use Symfony\AI\Platform\Tool\Tool;

/**
 * Converts between Drupal AI types and Symfony AI Platform types.
 */
class InputOutputConverter {

  /**
   * Converts a Drupal ChatInput to a Symfony MessageBag.
   *
   * @param \Drupal\ai\OperationType\Chat\ChatInput $input
   *   The Drupal chat input.
   *
   * @return \Symfony\AI\Platform\Message\MessageBag
   *   The Symfony message bag.
   */
  public function toMessageBag(ChatInput $input): MessageBag {
    $messages = [];

    // Add system prompt as a system message if present.
    if (!empty($input->getSystemPrompt())) {
      $messages[] = Message::forSystem($input->getSystemPrompt());
    }

    foreach ($input->getMessages() as $chatMessage) {
      $role = $chatMessage->getRole();
      switch ($role) {
        case 'system':
          $messages[] = Message::forSystem($chatMessage->getText());
          break;

        case 'user':
          $content = $this->buildUserContent($chatMessage);
          $messages[] = Message::ofUser(...$content);
          break;

        case 'assistant':
          $toolCalls = NULL;
          if ($chatMessage->getTools()) {
            $toolCalls = [];
            foreach ($chatMessage->getTools() as $tool) {
              $arguments = [];
              foreach ($tool->getArguments() as $arg) {
                $arguments[$arg->getName()] = $arg->getValue();
              }
              $toolCalls[] = new ToolCall(
                $tool->getToolId(),
                $tool->getName(),
                $arguments
              );
            }
          }
          $messages[] = Message::ofAssistant($chatMessage->getText() ?: NULL, $toolCalls ?: NULL);
          break;

        case 'tool':
          if ($chatMessage->getToolsId()) {
            $toolName = '';
            if ($chatMessage->getTools()) {
              $firstTool = reset($chatMessage->getTools());
              if ($firstTool) {
                $toolName = $firstTool->getName();
              }
            }
            $messages[] = Message::ofToolCall(
              new ToolCall($chatMessage->getToolsId(), $toolName),
              $chatMessage->getText()
            );
          }
          break;
      }
    }

    return new MessageBag(...$messages);
  }

  /**
   * Builds user message content parts from a ChatMessage.
   *
   * @param \Drupal\ai\OperationType\Chat\ChatMessage $chatMessage
   *   The chat message.
   *
   * @return array
   *   Array of content parts (Text, Image, etc.).
   */
  protected function buildUserContent(ChatMessage $chatMessage): array {
    $content = [];

    if (!empty($chatMessage->getText())) {
      $content[] = new Text($chatMessage->getText());
    }

    foreach ($chatMessage->getImages() as $imageFile) {
      $binary = $imageFile->getBinary();
      $mimeType = $imageFile->getMimeType();
      $base64 = base64_encode($binary);
      $dataUrl = 'data:' . $mimeType . ';base64,' . $base64;
      $content[] = Image::fromDataUrl($dataUrl);
    }

    // Fallback: if no content parts were created, use text.
    if (empty($content)) {
      $content[] = new Text('');
    }

    return $content;
  }

  /**
   * Builds Symfony options from a Drupal ChatInput.
   *
   * @param \Drupal\ai\OperationType\Chat\ChatInput $input
   *   The Drupal chat input.
   *
   * @return array
   *   The Symfony options array.
   */
  public function buildOptions(ChatInput $input): array {
    $options = [];

    if ($input->isStreamedOutput()) {
      $options['stream'] = TRUE;
    }

    if (!empty($input->getChatStructuredJsonSchema())) {
      $options['response_format'] = [
        'type' => 'json_schema',
        'json_schema' => $input->getChatStructuredJsonSchema(),
      ];
    }

    if ($input->getChatTools()) {
      $options['tools'] = $this->convertToolDefinitions($input->getChatTools());
    }

    return $options;
  }

  /**
   * Converts Drupal tool definitions to Symfony Tool objects.
   *
   * @param \Drupal\ai\OperationType\Chat\Tools\ToolsInputInterface $toolsInput
   *   The Drupal tools input.
   *
   * @return \Symfony\AI\Platform\Tool\Tool[]
   *   Array of Symfony Tool objects.
   */
  public function convertToolDefinitions(ToolsInputInterface $toolsInput): array {
    $tools = [];

    foreach ($toolsInput->getFunctions() as $function) {
      $parameters = NULL;
      $properties = $function->getProperties();

      if (!empty($properties)) {
        $schemaProperties = [];
        $required = [];

        foreach ($properties as $property) {
          $schemaProperties[$property->getName()] = $property->renderPropertyArray();
          if ($property->isRequired()) {
            $required[] = $property->getName();
          }
        }

        $parameters = [
          'type' => 'object',
          'properties' => $schemaProperties,
        ];
        if (!empty($required)) {
          $parameters['required'] = $required;
        }
      }

      // Use a no-op execution reference since Drupal handles tool execution.
      $reference = new ExecutionReference(self::class, 'noOpToolExecution');

      $tools[] = new Tool(
        $reference,
        $function->getName(),
        $function->getDescription() ?? '',
        $parameters
      );
    }

    return $tools;
  }

  /**
   * No-op tool execution placeholder.
   *
   * Tools are executed by Drupal's agent loop, not by Symfony.
   */
  public function noOpToolExecution(): string {
    return '';
  }

  /**
   * Converts a Symfony DeferredResult to a Drupal ChatOutput.
   *
   * @param \Symfony\AI\Platform\Result\DeferredResult $deferredResult
   *   The Symfony deferred result.
   *
   * @return \Drupal\ai\OperationType\Chat\ChatOutput
   *   The Drupal chat output.
   */
  public function toChatOutput(DeferredResult $deferredResult): ChatOutput {
    $result = $deferredResult->getResult();

    // Check if the result contains tool calls.
    if ($result instanceof ToolCallResult) {
      return $this->toolCallResultToChatOutput($deferredResult);
    }

    $text = $deferredResult->asText();
    $message = new ChatMessage('assistant', $text);
    $tokenUsage = $this->extractTokenUsage($deferredResult);

    return new ChatOutput($message, $result, $deferredResult->getMetadata()->all(), $tokenUsage);
  }

  /**
   * Converts a streamed Symfony DeferredResult to a Drupal ChatOutput.
   *
   * @param \Symfony\AI\Platform\Result\DeferredResult $deferredResult
   *   The Symfony deferred result with streaming enabled.
   *
   * @return \Drupal\ai\OperationType\Chat\ChatOutput
   *   The Drupal chat output wrapping a streamed iterator.
   */
  public function toStreamedChatOutput(DeferredResult $deferredResult): ChatOutput {
    $responseIterator = (static function () use ($deferredResult) {
      yield from $deferredResult->asStream();
    })();

    $message = new class($responseIterator) extends StreamedChatMessageIterator {

      /**
       * {@inheritdoc}
       */
      public function doIterate(): \Generator {
        foreach ($this->iterator as $chunk) {
          yield $this->createStreamedChatMessage(
            'assistant',
            $chunk,
            [],
            NULL,
            [],
          );
        }
      }

    };

    return new ChatOutput($message, $deferredResult, []);
  }

  /**
   * Converts a tool call result to a Drupal ChatOutput.
   *
   * @param \Symfony\AI\Platform\Result\DeferredResult $deferredResult
   *   The Symfony deferred result containing tool calls.
   *
   * @return \Drupal\ai\OperationType\Chat\ChatOutput
   *   The Drupal chat output with tool calls.
   */
  protected function toolCallResultToChatOutput(DeferredResult $deferredResult): ChatOutput {
    $symfonyToolCalls = $deferredResult->asToolCalls();
    $drupalTools = [];

    foreach ($symfonyToolCalls as $toolCall) {
      $output = new ToolsFunctionOutput(NULL, $toolCall->getId(), $toolCall->getArguments());
      $output->setName($toolCall->getName());
      $drupalTools[] = $output;
    }

    $message = new ChatMessage('assistant', '');
    $message->setTools($drupalTools);
    $tokenUsage = $this->extractTokenUsage($deferredResult);

    return new ChatOutput($message, $deferredResult->getResult(), $deferredResult->getMetadata()->all(), $tokenUsage);
  }

  /**
   * Extracts token usage from a deferred result's metadata.
   *
   * @param \Symfony\AI\Platform\Result\DeferredResult $deferredResult
   *   The Symfony deferred result.
   *
   * @return \Drupal\ai\Dto\TokenUsageDto
   *   The token usage DTO.
   */
  protected function extractTokenUsage(DeferredResult $deferredResult): TokenUsageDto {
    $metadata = $deferredResult->getMetadata();
    $usage = $metadata->get('token_usage');

    if (is_array($usage)) {
      return new TokenUsageDto(
        input: $usage['input'] ?? $usage['prompt_tokens'] ?? NULL,
        output: $usage['output'] ?? $usage['completion_tokens'] ?? NULL,
        total: $usage['total'] ?? $usage['total_tokens'] ?? NULL,
        reasoning: $usage['reasoning'] ?? $usage['reasoning_tokens'] ?? NULL,
        cached: $usage['cached'] ?? $usage['cached_tokens'] ?? NULL,
      );
    }

    return new TokenUsageDto();
  }

}
