<?php

declare(strict_types=1);

namespace Plugin\tnt_mcp\src\Mcp\Tools;

use Closure;
use Plugin\tnt_mcp\src\Mcp\Contracts\Tool;
use Plugin\tnt_mcp\src\Mcp\Journal\ChangeJournal;
use Plugin\tnt_mcp\src\Mcp\Mode\EndpointClass;

/**
 * READ tool: what this server has changed, newest first, and what each change can still undo.
 *
 * It exists before `undo_admin_action` can be useful at all — a `changeId` is opaque, and a caller
 * that has lost the one `invoke_admin_action` returned has no other way back to it. Headers only:
 * the before/after images stay in the journal until an undo actually needs them.
 *
 * `undoState` is the field to read. `undoable` is the only one that can be acted on; `undone`,
 * `superseded`, `expired` and `not-undoable` each come with the reason they are not.
 */
final class ListRecentChanges implements Tool
{
    /** How many entries a call returns when it does not say. */
    public const DEFAULT_LIMIT = 20;

    /** @param Closure(): ChangeJournal $journalFactory must not touch the DB until run() */
    public function __construct(private readonly Closure $journalFactory)
    {
    }

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

    public function description(): string
    {
        // Hard limit: 140 characters.
        return 'Changes made through this server, newest first, with what each one can and cannot '
            . 'undo. Feeds undo_admin_action.';
    }

    /** @return array<string,mixed> */
    public function inputSchema(): array
    {
        return [
            'type'       => 'object',
            'properties' => [
                'limit'     => [
                    'type'        => 'integer',
                    'description' => 'How many entries to return, newest first. Default '
                        . self::DEFAULT_LIMIT . ', maximum 200.',
                ],
                'actionId'  => [
                    'type'        => 'string',
                    'description' => 'Only changes made by this catalog action, e.g. '
                        . '"shipping.method.save".',
                ],
                'undoState' => [
                    'type'        => 'string',
                    'description' => 'Filter by state: undoable, undone, superseded, expired or '
                        . 'not-undoable.',
                ],
            ],
            '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
    {
        $limit     = \is_int($args['limit'] ?? null) ? (int)$args['limit'] : self::DEFAULT_LIMIT;
        $actionId  = \is_string($args['actionId'] ?? null) ? \trim((string)$args['actionId']) : null;
        $undoState = \is_string($args['undoState'] ?? null) ? \trim((string)$args['undoState']) : null;

        $changes = ($this->journalFactory)()->recent($limit, $actionId, $undoState);

        return [
            'changes' => $changes,
            'count'   => \count($changes),
            'notes'   => $changes === []
                ? ['No change has been journalled yet. Only invoke_admin_action writes this journal.']
                : [
                    'Only entries with undoState "undoable" can be passed to undo_admin_action; the '
                    . 'others carry the reason they cannot.',
                ],
        ];
    }
}
