<?php

declare(strict_types=1);

namespace Drupal\flowdrop_node_processor\Plugin\FlowDropNodeProcessor;

use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\flowdrop\Attribute\FlowDropNodeProcessor;
use Drupal\flowdrop\Constants\ReservedName;
use Drupal\flowdrop\DTO\ParameterBagInterface;
use Drupal\flowdrop\DTO\Tool\ToolResultInterface;
use Drupal\flowdrop\DTO\ValidationResult;
use Drupal\flowdrop\Plugin\FlowDropNodeProcessor\AbstractFlowDropNodeProcessor;
use Drupal\flowdrop\Plugin\FlowDropNodeProcessor\ToolsAwareInterface;
use Drupal\flowdrop\Plugin\FlowDropNodeProcessor\ToolsAwareTrait;

/**
 * Executes a batch of tool calls, via the native tool runtime.
 *
 * The loop-body half of a graph-authored agent: it reads the `tool_calls` a
 * reasoning step chose — the shape a tool-aware Reason node emits:
 * `[{name, args, tool_call_id}]` — and, for each, invokes the matching wired
 * tool through FlowDrop's `ScopedToolInvoker`
 * (`$tools->get($name)->invoke($args)`). Each call runs the chosen tool node as
 * a node execution on the shared session — auditable, with the allow-list
 * enforced by the wired `tool_availability` edges (no PHP allow-list, no
 * off-graph `$plugin->execute()`).
 *
 * EXPOSURE = TOPOLOGY: like every {@see ToolsAwareInterface} node, this must be
 * wired (via `tool_availability` edges) to exactly the tool nodes it may run —
 * the same set a paired reasoning node advertises to the model, so
 * "advertised == executable" is visible on the canvas.
 *
 * It emits one `tool`-role message per call ({role, tool_call_id, content}) so
 * the results can be appended to a conversation buffer paired to their calls;
 * the next reasoning iteration re-reads the buffer. An unknown/unwired tool is
 * a recoverable tool-error fed back to the model, not a hard failure.
 *
 * Unlike {@see ToolCaller} (the single-named-tool reference example), this node
 * executes a whole batch of model-chosen calls in one step, which is what an
 * agent loop needs.
 *
 * @see \Drupal\flowdrop_node_processor\Plugin\FlowDropNodeProcessor\ToolCaller
 * @see \Drupal\flowdrop\Plugin\FlowDropNodeProcessor\ToolsAwareInterface
 */
#[FlowDropNodeProcessor(
  id: "tool_invoke",
  label: new TranslatableMarkup("Invoke tools"),
  description: "Run a batch of tool calls (each an auditable node execution via the tool invoker)",
  version: "1.0.0",
)]
class ToolInvoke extends AbstractFlowDropNodeProcessor implements ToolsAwareInterface {

  use ToolsAwareTrait;

  /**
   * {@inheritdoc}
   */
  public function process(ParameterBagInterface $params): array {
    $toolCalls = $params->getArray('tool_calls', []);
    $tools = $this->getTools();

    $messages = [];
    $results = [];
    $ok = TRUE;
    foreach ($toolCalls as $call) {
      if (!is_array($call)) {
        continue;
      }
      $name = trim((string) ($call['name'] ?? ''));
      if ($name === '') {
        continue;
      }
      $args = is_array($call['args'] ?? NULL) ? $call['args'] : [];
      $toolCallId = (string) ($call['tool_call_id'] ?? '');

      // An unknown/unwired tool name is recoverable: tell the model so it can
      // re-plan (the wiring is the allow-list — never trust a forged name).
      $binding = $tools->get($name);
      if ($binding === NULL) {
        $content = sprintf('Error: "%s" is not an available tool.', $name);
        $ok = FALSE;
      }
      else {
        $result = $binding->invoke($args);
        $content = $this->resultToString($result);
        $ok = $ok && $result->isSuccess();
      }

      $messages[] = [
        'role' => 'tool',
        'tool_call_id' => $toolCallId,
        'content' => $content,
      ];
      $results[] = [
        'name' => $name,
        'tool_call_id' => $toolCallId,
        'content' => $content,
      ];
    }

    return [
      'tool_messages' => $messages,
      'tool_results' => $results,
      'ok' => $ok,
    ];
  }

  /**
   * Render a tool result as the string content the model reads back.
   *
   * A node's result whose data carries a `result` key surfaces that; any other
   * tool's data is JSON-encoded. A failed result becomes an "Error: …" string
   * the model can recover from.
   */
  private function resultToString(ToolResultInterface $result): string {
    if (!$result->isSuccess()) {
      return 'Error: ' . ($result->getError() ?? 'the tool failed.');
    }
    $data = $result->getData();
    if (array_key_exists('result', $data)) {
      return (string) $data['result'];
    }
    return (string) json_encode($data);
  }

  /**
   * {@inheritdoc}
   */
  public function validateParams(array $params): ValidationResult {
    return ValidationResult::success();
  }

  /**
   * {@inheritdoc}
   */
  public function getParameterSchema(): array {
    return [
      'type' => 'object',
      'properties' => [
        'tool_calls' => [
          'type' => 'array',
          'title' => 'Tool calls',
          'description' => 'The tool calls to run: [{name, args, tool_call_id}]. Each is invoked against the wired tools.',
          'default' => [],
          'required' => FALSE,
        ],
        // The tool input port (ReservedName::PORT_TOOL): the tools this node
        // may execute, wired tool.output → invoke.input-tool. Wire the same
        // tool set the paired reasoning node advertised — "advertised ==
        // executable", enforced by the topology. Marked connectable in config.
        ReservedName::PORT_TOOL => [
          'type' => 'tool',
          'title' => 'Tools',
          'description' => 'Tools this node may execute. Wire the same tool set as the paired reasoning node.',
          'required' => FALSE,
        ],
      ],
    ];
  }

  /**
   * {@inheritdoc}
   */
  public function getOutputSchema(): array {
    return [
      'type' => 'object',
      'properties' => [
        'tool_messages' => [
          'type' => 'array',
          'description' => 'One tool-role message per call ({role, tool_call_id, content}) — append to a conversation buffer to persist.',
        ],
        'tool_results' => [
          'type' => 'array',
          'description' => 'Per-call results ({name, tool_call_id, content}).',
        ],
        'ok' => [
          'type' => 'boolean',
          'description' => 'TRUE if every tool call succeeded.',
        ],
      ],
    ];
  }

}
