<?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: one backend action's full contract — the `read-logs-undo-guidance.md` §5.2 shape.
 *
 * This is where the guidance is *spent*: prerequisites, an ordered recipe, the co-requirement rules
 * the caller is meant to validate against, the pitfalls, every field, and — before anything is
 * invoked — the side effects a rollback cannot reach (§3.4 point 1) and the `undo:` block with its
 * `cannotUndo` list (§4.7 point 1). A `changeId` promised without that list is the one thing this
 * feature must not imply, and the promise starts here, not at the invocation.
 *
 * Capped at 1200 tokens by {@see ActionDescriber}, which applies §5.1's truncation order. `verbose:
 * true` is the escape hatch and is not capped.
 *
 * Reversible: it reads a YAML file. Describing an action does not perform it.
 */
final class DescribeAdminAction 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 'describe_admin_action';
    }

    public function description(): string
    {
        // Hard limit: 140 characters. Tool descriptions are prompt, paid on every request.
        return 'Full contract for one admin action: prerequisites, recipe, every field, side '
            . 'effects and what an undo cannot reverse. Read before invoking.';
    }

    /** @return array<string,mixed> */
    public function inputSchema(): array
    {
        return [
            'type'       => 'object',
            'properties' => [
                'id'         => [
                    'type'        => 'string',
                    'description' => 'Action id as returned by list_admin_actions, '
                        . 'e.g. "shipping.method.save".',
                ],
                'verbose'    => [
                    'type'        => 'boolean',
                    'description' => 'Return the uncapped contract: the optional fields that were '
                        . 'collapsed to a "+ N more optional fields" line, and the full recipe prose.',
                ],
                'enumFilter' => [
                    'type'        => 'string',
                    'description' => 'Case-insensitive substring filter on enum values, for the '
                        . 'long lists (country codes). Values are only present when the caller '
                        . 'supplied them; this tool resolves none itself.',
                ],
            ],
            'required'             => ['id'],
            'additionalProperties' => false,
        ];
    }

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

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

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

    /**
     * An unknown id answers with `error` + `knownIds` rather than throwing: the caller guessed an
     * id, which is a normal step, and a list of the real ones costs less than a failed tool call.
     *
     * @param array<string,mixed> $args
     * @return array<string,mixed>
     */
    public function run(array $args): array
    {
        $id = $args['id'] ?? null;
        if (!\is_string($id) || \trim($id) === '') {
            throw new RuntimeException('describe_admin_action: "id" is required');
        }
        $verbose = $args['verbose'] ?? false;
        if (!\is_bool($verbose)) {
            throw new RuntimeException('describe_admin_action: "verbose" must be a boolean');
        }
        $filter = $args['enumFilter'] ?? null;
        if ($filter !== null && !\is_string($filter)) {
            throw new RuntimeException('describe_admin_action: "enumFilter" must be a string');
        }

        return ($this->describerFactory)()->describe(\trim($id), $verbose, $filter);
    }
}
