<?php

/*
 * This file is part of the official PHP MCP SDK.
 *
 * A collaboration between Symfony and the PHP Foundation.
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */
namespace Matomo\Dependencies\McpServer\Mcp\Server\Transport\Http\Middleware;

use Matomo\Dependencies\McpServer\Http\Discovery\Psr17FactoryDiscovery;
use Matomo\Dependencies\McpServer\Mcp\Exception\ClientRegistrationException;
use Matomo\Dependencies\McpServer\Mcp\Exception\InvalidArgumentException;
use Matomo\Dependencies\McpServer\Mcp\Server\Transport\Http\OAuth\ClientRegistrarInterface;
use Matomo\Dependencies\McpServer\Psr\Http\Message\ResponseFactoryInterface;
use Matomo\Dependencies\McpServer\Psr\Http\Message\ResponseInterface;
use Matomo\Dependencies\McpServer\Psr\Http\Message\ServerRequestInterface;
use Matomo\Dependencies\McpServer\Psr\Http\Message\StreamFactoryInterface;
use Matomo\Dependencies\McpServer\Psr\Http\Server\MiddlewareInterface;
use Matomo\Dependencies\McpServer\Psr\Http\Server\RequestHandlerInterface;
/**
 * OAuth 2.0 Dynamic Client Registration (RFC 7591) middleware.
 *
 * Handles POST /register requests by delegating to a ClientRegistrarInterface
 * and enriches /.well-known/oauth-authorization-server responses with the
 * registration_endpoint.
 */
final class ClientRegistrationMiddleware implements MiddlewareInterface
{
    private const REGISTRATION_PATH = '/register';
    private ResponseFactoryInterface $responseFactory;
    private StreamFactoryInterface $streamFactory;
    public function __construct(private readonly ClientRegistrarInterface $registrar, private readonly string $localBaseUrl, ?ResponseFactoryInterface $responseFactory = null, ?StreamFactoryInterface $streamFactory = null)
    {
        if ('' === trim($localBaseUrl)) {
            throw new InvalidArgumentException('The $localBaseUrl must not be empty.');
        }
        $this->responseFactory = $responseFactory ?? Psr17FactoryDiscovery::findResponseFactory();
        $this->streamFactory = $streamFactory ?? Psr17FactoryDiscovery::findStreamFactory();
    }
    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler) : ResponseInterface
    {
        $path = $request->getUri()->getPath();
        if ('POST' === $request->getMethod() && self::REGISTRATION_PATH === $path) {
            return $this->handleRegistration($request);
        }
        $response = $handler->handle($request);
        if ('GET' === $request->getMethod() && '/.well-known/oauth-authorization-server' === $path) {
            return $this->enrichAuthServerMetadata($response);
        }
        return $response;
    }
    private function handleRegistration(ServerRequestInterface $request) : ResponseInterface
    {
        $contentType = $request->getHeaderLine('Content-Type');
        if (!str_starts_with($contentType, 'application/json')) {
            return $this->jsonResponse(400, ['error' => 'invalid_client_metadata', 'error_description' => 'Content-Type must be application/json.'], ['Cache-Control' => 'no-store']);
        }
        $body = $request->getBody()->__toString();
        try {
            $decoded = json_decode($body, \false, 512, \JSON_THROW_ON_ERROR);
        } catch (\JsonException) {
            return $this->jsonResponse(400, ['error' => 'invalid_client_metadata', 'error_description' => 'Request body must be valid JSON.'], ['Cache-Control' => 'no-store']);
        }
        if (!$decoded instanceof \stdClass) {
            return $this->jsonResponse(400, ['error' => 'invalid_client_metadata', 'error_description' => 'Request body must be a JSON object.'], ['Cache-Control' => 'no-store']);
        }
        // Re-decode with assoc=true so nested objects become arrays (safe — already validated above)
        /** @var array<string, mixed> $data */
        $data = json_decode($body, \true, 512, \JSON_THROW_ON_ERROR);
        try {
            $result = $this->registrar->register($data);
        } catch (ClientRegistrationException $e) {
            return $this->jsonResponse(400, ['error' => $e->errorCode, 'error_description' => $e->getMessage()], ['Cache-Control' => 'no-store']);
        }
        return $this->jsonResponse(201, $result, ['Cache-Control' => 'no-store']);
    }
    private function enrichAuthServerMetadata(ResponseInterface $response) : ResponseInterface
    {
        if (200 !== $response->getStatusCode()) {
            return $response;
        }
        $stream = $response->getBody();
        if ($stream->isSeekable()) {
            $stream->rewind();
        }
        try {
            $metadata = json_decode($stream->__toString(), \true, 512, \JSON_THROW_ON_ERROR);
        } catch (\JsonException) {
            if ($stream->isSeekable()) {
                $stream->rewind();
            }
            return $response;
        }
        if (!\is_array($metadata) || [] !== $metadata && array_is_list($metadata)) {
            if ($stream->isSeekable()) {
                $stream->rewind();
            }
            return $response;
        }
        $metadata['registration_endpoint'] = rtrim($this->localBaseUrl, '/') . self::REGISTRATION_PATH;
        return $response->withBody($this->streamFactory->createStream(json_encode($metadata, \JSON_THROW_ON_ERROR | \JSON_UNESCAPED_SLASHES)))->withHeader('Content-Type', 'application/json')->withoutHeader('Content-Length');
    }
    /**
     * @param array<string, mixed>  $data
     * @param array<string, string> $extraHeaders
     */
    private function jsonResponse(int $status, array $data, array $extraHeaders = []) : ResponseInterface
    {
        $response = $this->responseFactory->createResponse($status)->withHeader('Content-Type', 'application/json')->withBody($this->streamFactory->createStream(json_encode($data, \JSON_THROW_ON_ERROR | \JSON_UNESCAPED_SLASHES)));
        foreach ($extraHeaders as $name => $value) {
            if ('' !== $value) {
                $response = $response->withHeader($name, $value);
            }
        }
        return $response;
    }
}
