<?php

declare(strict_types=1);

namespace App\Mcp\EventListener;

use App\Mcp\Schema\CoercingSchemaGenerator;
use Mcp\Capability\RegistryInterface;
use Mcp\Event\RequestEvent;
use Mcp\Schema\Request\CallToolRequest;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Component\EventDispatcher\Attribute\AsEventListener;
use Throwable;

use function is_array;
use function is_string;

/**
 * Decodes JSON-encoded structured arguments before tool calls are validated.
 *
 * Some MCP clients/proxies serialize array and object arguments as JSON strings
 * (e.g. `tagIds` arrives as the string `"[3]"` instead of the array `[3]`). The
 * SDK validates arguments against the JSON Schema BEFORE casting, so an `array`
 * schema rejects the string with a 422, and ReferenceHandler::castToArray does
 * not decode JSON strings either.
 *
 * This listener runs on the SDK RequestEvent (dispatched before any handler) and,
 * driven by the tool's input schema, decodes string arguments whose target type
 * is `array` or `object`. Scalar stringification is handled separately by
 * {@see CoercingSchemaGenerator}.
 */
#[AsEventListener(event: RequestEvent::class)]
final class CoerceJsonEncodedArgumentsListener
{
    public function __construct(
        #[Autowire(service: 'mcp.registry')]
        private readonly RegistryInterface $registry,
    ) {}

    public function __invoke(RequestEvent $event): void
    {
        $request = $event->getRequest();
        if (!$request instanceof CallToolRequest) {
            return;
        }

        $arguments = $request->arguments;
        if ([] === $arguments) {
            return;
        }

        $properties = $this->toolProperties($request->name);
        if (null === $properties) {
            return;
        }

        $changed = false;
        foreach ($arguments as $name => $value) {
            if (!is_string($value) || !is_array($properties[$name] ?? null)) {
                continue;
            }

            $types = (array) ($properties[$name]['type'] ?? []);
            if ([] === array_intersect(['array', 'object'], $types)) {
                continue;
            }

            $decoded = json_decode($value, true);
            if (is_array($decoded)) {
                $arguments[$name] = $decoded;
                $changed          = true;
            }
        }

        if ($changed) {
            $event->setRequest(
                new CallToolRequest($request->name, $arguments)
                    ->withId($request->getId())
                    ->withMeta($request->getMeta()),
            );
        }
    }

    /**
     * @return null|array<string, mixed>
     */
    private function toolProperties(string $toolName): ?array
    {
        try {
            $schema = $this->registry->getTool($toolName)->tool->inputSchema;
        } catch (Throwable) {
            return null;
        }

        $properties = $schema['properties'] ?? null;

        return is_array($properties) ? $properties : null;
    }
}
