<?php

declare(strict_types=1);

namespace Plugin\tnt_mcp\src\Mcp\Tools;

use Closure;
use Plugin\tnt_mcp\src\Mcp\Backend\LegacyTokenException;
use Plugin\tnt_mcp\src\Mcp\Backend\UnknownAdminException;
use Plugin\tnt_mcp\src\Mcp\Contracts\Tool;
use Plugin\tnt_mcp\src\Mcp\Contracts\TokenAwareTool;
use Plugin\tnt_mcp\src\Mcp\Journal\UndoService;
use Plugin\tnt_mcp\src\Mcp\Mode\EndpointClass;
use RuntimeException;

/**
 * WRITE tool: restore the rows one journalled change touched (design §4.6).
 *
 * ## Plan by default
 *
 * Without `confirm: true` it returns the plan and writes nothing. The blast radius of an undo is
 * invisible from its arguments — `changeId` is an opaque string — so the caller has to be shown what
 * it is about to revert before it can agree to it.
 *
 * ## Destructive, and denied in `limited` mode on purpose
 *
 * It writes, and it can destroy work done after the change it reverts. A shop that has not opted
 * into write mode must not be able to undo either: an undo is not a smaller operation than the
 * action it reverses, it is the same operation pointed backwards.
 *
 * ## No fallback identity
 *
 * Like {@see InvokeAdminAction}, it acts as the administrator recorded on the presented token, and a
 * token that records nobody is a refusal rather than a default. `run()` therefore throws — the
 * inherited entry point has no token, so it has no identity.
 */
final class UndoAdminAction implements Tool, TokenAwareTool
{
    private ?UndoService $service = null;

    /**
     * @param Closure(): UndoService  $serviceFactory must not parse the catalog or touch the
     *                                                container until a real call
     * @param Closure(string): string $loginResolver  token audit id => `tadminlogin.cLogin`
     */
    public function __construct(
        private readonly Closure $serviceFactory,
        private readonly Closure $loginResolver,
    ) {
    }

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

    public function description(): string
    {
        // Hard limit: 140 characters.
        return 'Undo one change from list_recent_changes. Returns the plan first and applies '
            . 'nothing without confirm: true.';
    }

    /** @return array<string,mixed> */
    public function inputSchema(): array
    {
        return [
            'type'       => 'object',
            'properties' => [
                'changeId' => [
                    'type'        => 'string',
                    'description' => 'The changeId invoke_admin_action returned, or one from '
                        . 'list_recent_changes.',
                ],
                'confirm'  => [
                    'type'        => 'boolean',
                    'description' => 'Without it the tool returns the plan and changes nothing. With '
                        . 'it the plan is applied in one transaction.',
                ],
            ],
            'required'             => ['changeId'],
            'additionalProperties' => false,
        ];
    }

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

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

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

    /**
     * @param array<string,mixed> $args
     * @return array<string,mixed>
     */
    public function run(array $args): array
    {
        throw new RuntimeException(
            'undo_admin_action cannot run without a token: it acts as the backend administrator '
            . 'recorded on the presented OAuth token, and there is no fallback identity by design. '
            . 'Dispatch it through Server::dispatch(), which supplies one.',
        );
    }

    /**
     * @param array<string,mixed> $args
     * @return array<string,mixed>
     */
    public function runForToken(array $args, string $tokenId): array
    {
        $changeId = $args['changeId'] ?? null;
        if (!\is_string($changeId) || \trim($changeId) === '') {
            throw new RuntimeException('undo_admin_action: "changeId" is required');
        }
        $confirm = $args['confirm'] ?? false;
        if (!\is_bool($confirm)) {
            throw new RuntimeException('undo_admin_action: "confirm" must be a boolean');
        }

        try {
            $login = ($this->loginResolver)($tokenId);
        } catch (LegacyTokenException | UnknownAdminException $e) {
            return [
                'ok'       => false,
                'applied'  => false,
                'refused'  => 'no-acting-admin',
                'error'    => $e->getMessage(),
                'changeId' => \trim($changeId),
                'notes'    => [
                    'Nothing was undone. This plugin never substitutes a default administrator.',
                ],
            ];
        }

        return $this->service()->undo(\trim($changeId), $confirm, $tokenId, $login);
    }

    /** Built once per process and reused: it parses the whole action catalog. */
    private function service(): UndoService
    {
        return $this->service ??= ($this->serviceFactory)();
    }
}
