<?php

declare(strict_types=1);

namespace Plugin\tnt_mcp\src\Mcp\Tools;

use Closure;
use Plugin\tnt_mcp\src\Mcp\Action\ActionDescriber;
use Plugin\tnt_mcp\src\Mcp\Contracts\Tool;
use Plugin\tnt_mcp\src\Mcp\Mode\EndpointClass;
use RuntimeException;

/**
 * READ tool: the index of every backend action this shop exposes to `invoke_admin_action`.
 *
 * The counterpart of {@see ListTables} on the write side, and the same reasoning: nobody names
 * `shipping.method.save` from memory, and paying for all five contracts on every request to make
 * one discoverable is the wall-of-text design `read-logs-undo-guidance.md` §5.1 exists to reject.
 *
 * Two steps, because the first call knows least. With no `area` this answers "where do I look" —
 * a map of the areas, how many actions each holds and which guide explains it, ~265 tokens that do
 * not grow as the catalog does. With an `area` it answers "what is there" in id + area + title +
 * reversibility + side effects, ≤800 tokens. {@see DescribeAdminAction} answers "how" after that.
 *
 * Reversible: it reads a YAML file. Nothing here can invoke anything.
 */
final class ListAdminActions implements Tool
{
    /**
     * @param Closure(): ActionDescriber $describerFactory must not parse the catalog until run() —
     *                                   {@see \Plugin\tnt_mcp\src\Mcp\OAuth\ScopeCatalog} builds
     *                                   every tool just to read its scope()
     */
    public function __construct(private readonly Closure $describerFactory)
    {
    }

    public function name(): string
    {
        return 'list_admin_actions';
    }

    public function description(): string
    {
        // Hard limit: 140 characters. Tool descriptions are prompt, paid on every request.
        return 'Start here for backend changes: a map of the admin areas this shop can change, then '
            . 'one area\'s actions with side effects and reversibility.';
    }

    /** @return array<string,mixed> */
    public function inputSchema(): array
    {
        // `area` is a free string rather than a JSON-schema enum on purpose: filling an enum would
        // mean parsing the catalog during tools/list, on every request, to save the caller one
        // retry. The response carries the area list instead.
        return [
            'type'       => 'object',
            'properties' => [
                'area'       => [
                    'type'        => 'string',
                    'description' => 'Return this area\'s actions (the id prefix: "shipping", '
                        . '"payment", "coupon"). Omit to get the map of every area first.',
                ],
                'writesOnly' => [
                    'type'        => 'boolean',
                    'description' => 'Return only actions that change shop state. Every catalog '
                        . 'action does, so this currently filters nothing.',
                ],
            ],
            'additionalProperties' => false,
        ];
    }

    public function isWrite(): bool
    {
        return false;
    }

    public function scope(): string
    {
        return 'actions:read';
    }

    public function endpointClass(): EndpointClass
    {
        return EndpointClass::Reversible;
    }

    /**
     * @param array<string,mixed> $args
     * @return array<string,mixed>
     */
    public function run(array $args): array
    {
        $area = $args['area'] ?? null;
        if ($area !== null && !\is_string($area)) {
            throw new RuntimeException('list_admin_actions: "area" must be a string');
        }
        $writesOnly = $args['writesOnly'] ?? false;
        if (!\is_bool($writesOnly)) {
            throw new RuntimeException('list_admin_actions: "writesOnly" must be a boolean');
        }

        return ($this->describerFactory)()->summarise($area, $writesOnly);
    }
}
