<?php

declare(strict_types=1);

namespace App\Mcp\Schema;

use Mcp\Capability\Discovery\DocBlockParser;
use Mcp\Capability\Discovery\SchemaGenerator;
use Mcp\Capability\Discovery\SchemaGeneratorInterface;
use Reflector;

use function count;
use function in_array;
use function is_array;

/**
 * Wraps the SDK SchemaGenerator and relaxes scalar parameter schemas so that
 * numeric/boolean parameters also accept their string representation.
 *
 * Rationale: some MCP clients serialize every JSON-RPC argument as a string
 * (e.g. `"22"` instead of `22`). The SDK validates arguments against the
 * generated JSON Schema BEFORE casting them (see CallToolHandler), so a strict
 * `integer` schema rejects `"22"` with a 422 even though the SDK's
 * ReferenceHandler::castArgumentType would happily coerce it afterwards.
 *
 * By advertising `["integer", "string"]` (resp. number/boolean) we let opis
 * accept the stringified value; the reflected PHP type hint (`int`, `bool`, ...)
 * still drives the actual coercion in ReferenceHandler. Non-numeric strings are
 * rejected later with a clear "cannot cast" error.
 */
final class CoercingSchemaGenerator implements SchemaGeneratorInterface
{
    public function __construct(
        private readonly SchemaGeneratorInterface $inner = new SchemaGenerator(new DocBlockParser()),
    ) {}

    public function generate(Reflector $reflection): array
    {
        $schema = $this->inner->generate($reflection);

        if (isset($schema['properties']) && is_array($schema['properties'])) {
            foreach ($schema['properties'] as $name => $property) {
                if (is_array($property)) {
                    $schema['properties'][$name] = $this->relaxNode($property);
                }
            }
        }

        return $schema;
    }

    public function generateOutputSchema(Reflector $reflection): ?array
    {
        return $this->inner->generateOutputSchema($reflection);
    }

    /**
     * @param array<string, mixed> $node
     *
     * @return array<string, mixed>
     */
    private function relaxNode(array $node): array
    {
        if (isset($node['type'])) {
            $node['type'] = $this->relaxType($node['type']);
        }

        // Relax array element types too (stringified IDs inside tagIds, etc.).
        if (isset($node['items']) && is_array($node['items'])) {
            $node['items'] = $this->relaxNode($node['items']);
        }

        return $node;
    }

    /**
     * Adds "string" to a type definition that allows integer/number/boolean.
     *
     * @param string|string[] $type
     *
     * @return string|string[]
     */
    private function relaxType(array|string $type): array|string
    {
        $types = (array) $type;

        $isNumericOrBool = in_array('integer', $types, true)
            || in_array('number', $types, true)
            || in_array('boolean', $types, true);

        if ($isNumericOrBool && !in_array('string', $types, true)) {
            $types[] = 'string';
        }

        return 1 === count($types) ? $types[0] : array_values($types);
    }
}
