<?php

declare(strict_types=1);

namespace Drupal\flowdrop\DTO\Reason;

/**
 * One conversation message handed to (or replayed for) a reasoning step.
 *
 * FlowDrop-owned, provider-neutral: it is NOT a `drupal/ai` `ChatMessage`. A
 * ChatReasonerInterface implementation
 * translates these into whatever its backend needs. An assistant turn may carry
 * `toolCalls` (the calls the model requested); a `tool` turn carries the
 * `toolCallId` it answers (providers reject an unpaired tool turn).
 *
 * @api
 */
final class ReasonMessage {

  /**
   * Constructs a ReasonMessage.
   *
   * @param string $role
   *   One of: system, user, assistant, tool.
   * @param string $content
   *   The message prose (may be empty on an assistant tool-call turn).
   * @param array<int, array<string, mixed>> $toolCalls
   *   Assistant turns only: requested calls, each {name, args, tool_call_id}.
   * @param string|null $toolCallId
   *   Tool turns only: the id of the call this message answers.
   */
  public function __construct(
    private readonly string $role,
    private readonly string $content = '',
    private readonly array $toolCalls = [],
    private readonly ?string $toolCallId = NULL,
  ) {}

  /**
   * Builds a message from a raw conversation-buffer array.
   *
   * @param array<string, mixed> $data
   *   A raw message: {role, content?, tool_calls?, tool_call_id?}.
   *
   * @return self
   *   The message.
   */
  public static function fromArray(array $data): self {
    $toolCalls = [];
    if (!empty($data['tool_calls']) && is_array($data['tool_calls'])) {
      foreach ($data['tool_calls'] as $call) {
        if (is_array($call)) {
          $toolCalls[] = $call;
        }
      }
    }
    $toolCallId = isset($data['tool_call_id']) && $data['tool_call_id'] !== ''
      ? (string) $data['tool_call_id']
      : NULL;
    return new self(
      (string) ($data['role'] ?? ''),
      (string) ($data['content'] ?? ''),
      $toolCalls,
      $toolCallId,
    );
  }

  /**
   * The message role.
   *
   * @return string
   *   One of: system, user, assistant, tool.
   */
  public function getRole(): string {
    return $this->role;
  }

  /**
   * The message prose.
   *
   * @return string
   *   The content.
   */
  public function getContent(): string {
    return $this->content;
  }

  /**
   * The tool calls this (assistant) turn requested.
   *
   * @return array<int, array<string, mixed>>
   *   Each call is {name, args, tool_call_id}.
   */
  public function getToolCalls(): array {
    return $this->toolCalls;
  }

  /**
   * The id of the call this (tool) turn answers.
   *
   * @return string|null
   *   The tool call id, or NULL for non-tool turns.
   */
  public function getToolCallId(): ?string {
    return $this->toolCallId;
  }

  /**
   * Renders the message back to a plain conversation-buffer array.
   *
   * Only the keys relevant to the role are included, so the output round-trips
   * through {@see self::fromArray()} without introducing empty artifacts.
   *
   * @return array<string, mixed>
   *   The raw message.
   */
  public function toArray(): array {
    $out = ['role' => $this->role, 'content' => $this->content];
    if ($this->toolCalls !== []) {
      $out['tool_calls'] = $this->toolCalls;
    }
    if ($this->toolCallId !== NULL) {
      $out['tool_call_id'] = $this->toolCallId;
    }
    return $out;
  }

}
