<?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\Plugin\FlowDropNodeProcessor\AbstractFlowDropNodeProcessor;
use Drupal\flowdrop\Plugin\FlowDropNodeProcessor\ToolsAwareInterface;
use Drupal\flowdrop\Plugin\FlowDropNodeProcessor\ToolsAwareTrait;

/**
 * Reference processor for the tool-consumer capability.
 *
 * A minimal, deterministic example of a {@see ToolsAwareInterface} processor.
 * It discovers the tools wired to its node (via tool_availability edges) and,
 * if a tool name is given, invokes that tool and returns the result. It always
 * exposes the available tool definitions, which is exactly what a real agent
 * would hand to an LLM as its tool manifest.
 *
 * This is the canonical "wire a tool, then call it" example. A real agent would
 * replace the static `tool_name`/`arguments` inputs with an LLM loop, but the
 * tool interaction — `getTools()`, `get($name)`, `invoke($args)` — is
 * identical. The reserved `tool` input port (declared in the parameter schema
 * with the `tool` data type) is what makes the node wireable as a tool
 * consumer in the editor.
 *
 * @see \Drupal\flowdrop\Plugin\FlowDropNodeProcessor\ToolsAwareInterface
 * @see \Drupal\flowdrop\DTO\Tool\ToolCollectionInterface
 *
 * @internal This class is not part of the public API and may change without notice.
 */
#[FlowDropNodeProcessor(
  id: "tool_caller",
  label: new TranslatableMarkup("Tool Caller"),
  description: "Invoke a tool wired to this node and return its result (reference example for ToolsAwareInterface)",
  version: "1.0.0"
)]
class ToolCaller extends AbstractFlowDropNodeProcessor implements ToolsAwareInterface {

  use ToolsAwareTrait;

  /**
   * {@inheritdoc}
   */
  public function process(ParameterBagInterface $params): array {
    $tools = $this->getTools();
    // The tool manifest a real agent would expose to its model.
    $available = $tools->getDefinitions();

    $toolName = (string) $params->get("tool_name", "");
    if ($toolName === "") {
      // No tool requested — just surface what is available (discovery).
      return [
        "available_tools" => $available,
        "called" => NULL,
        "result" => NULL,
      ];
    }

    // Resolve the requested tool by its model-facing name. An unknown name is a
    // normal, recoverable condition (e.g. a hallucinated tool call).
    $binding = $tools->get($toolName);
    if ($binding === NULL) {
      return [
        "available_tools" => $available,
        "called" => $toolName,
        "success" => FALSE,
        "error" => sprintf('Unknown tool "%s".', $toolName),
        "result" => NULL,
      ];
    }

    $arguments = $params->get("arguments", []);
    if (!is_array($arguments)) {
      $arguments = [];
    }

    // The orchestrator authorizes and runs the tool node; we just read back the
    // result. A failed result is recoverable, not an exception.
    $result = $binding->invoke($arguments);

    return [
      "available_tools" => $available,
      "called" => $toolName,
      "success" => $result->isSuccess(),
      "result" => $result->getData(),
      "error" => $result->getError(),
    ];
  }

  /**
   * {@inheritdoc}
   */
  public function getParameterSchema(): array {
    return [
      "type" => "object",
      "properties" => [
        // The reserved tool input port (dataType "tool"): this is what lets a
        // site builder wire tool nodes into this consumer in the editor. The
        // reserved name drives runtime edge classification via the
        // `{nodeId}-input-tool` handle — see the tool-aware docs.
        ReservedName::PORT_TOOL => [
          "type" => "tool",
          "description" => "Tools wired to this node.",
        ],
        "tool_name" => [
          "type" => "string",
          "description" => "The model-facing name of the wired tool to invoke. Leave empty to only list available tools.",
          "default" => "",
        ],
        "arguments" => [
          "type" => "object",
          "description" => "Arguments passed to the tool, matching its input schema.",
          "default" => [],
        ],
      ],
    ];
  }

  /**
   * {@inheritdoc}
   */
  public function getOutputSchema(): array {
    return [
      "type" => "object",
      "properties" => [
        "available_tools" => [
          "type" => "array",
          "description" => "Definitions (name, description, input_schema) of every wired tool.",
        ],
        "called" => [
          "type" => "string",
          "description" => "The tool that was invoked, if any.",
        ],
        "success" => [
          "type" => "boolean",
          "description" => "Whether the tool invocation succeeded.",
        ],
        "result" => [
          "type" => "object",
          "description" => "The tool's output data.",
        ],
        "error" => [
          "type" => "string",
          "description" => "Error message when the tool was unknown or failed.",
        ],
      ],
    ];
  }

}
