<?php
namespace Modules\RemoteData\Mcp;

use GuzzleHttp\Psr7\HttpFactory;
use GuzzleHttp\Psr7\ServerRequest;
use Mcp\Capability\Registry\Container;
use Mcp\Schema\ServerCapabilities;
use Mcp\Server;
use Mcp\Server\Session\FileSessionStore;
use Mcp\Server\Transport\StreamableHttpTransport;
use Mcp\Schema\Enum\ProtocolVersion;

//use Laminas\HttpHandlerRunner\Emitter\SapiEmitter;

/**
 * @see https://github.com/modelcontextprotocol/php-sdk/blob/main/README.md
 */
class Controller extends \App\Mvc\ControllerAbstract
{

    /**
     * @var \GuzzleHttp\Psr7\HttpFactory
     */
    protected $factory;

    /**
     * @var \GuzzleHttp\Psr7\ServerRequest
     */
    protected $request;

    /**
     * @var \GuzzleHttp\Psr7\Response
     */
    protected $response;

    /**
     * @var \Modules\RemoteData\Mcp\Logger
     */
    protected $logger;

    /**
     * @var \Engine\Cache\Disk
     */
    protected $cache;

    /**
     * @var \Mcp\Capability\Registry\Container
     */
    protected $container;

    /**
     * @var \Mcp\Server\Session\FileSessionStore
     */
    protected $session;

    /**
     * @var \Mcp\Server\Transport\StreamableHttpTransport
     */
    protected $transport;


    /**
     * {@inheritDoc}
     * @see \App\Mvc\ControllerAbstract::initialize()
     */
    protected function initialize()
    {
        $CFG = \Engine\Loader_Cfg::getInstance()->getGlobal();

        parent::initialize();
        $this->actionsCfg['index'] = ['authType' => 'none', 'checkSection' => false, 'checkOption' => false,];

        // Examples
        $this->actionsCfg['explicitRegistration'] = ['authType' => 'none', 'checkSection' => false, 'checkOption' => false,];
        $this->actionsCfg['complexToolSchema'] = ['authType' => 'none', 'checkSection' => false, 'checkOption' => false,];
        $this->actionsCfg['customDependencies'] = ['authType' => 'none', 'checkSection' => false, 'checkOption' => false,];

        // TODO OAuth2
        $this->actionsCfg['oauthProtectedResource'] = ['authType' => 'none', 'checkSection' => false, 'checkOption' => false,];
        $this->actionsCfg['oauthAuthorizationServer'] = ['authType' => 'none', 'checkSection' => false, 'checkOption' => false,];
        $this->actionsCfg['authToken'] = ['authType' => 'none', 'checkSection' => false, 'checkOption' => false,];

        // default /authorize endpoint
        $this->actionsCfg['authorize'] = ['authType' => 'none', 'checkSection' => false, 'checkOption' => false,];
        // Exchange code → access token endpoint
        $this->actionsCfg['token'] = ['authType' => 'none', 'checkSection' => false, 'checkOption' => false,];
        // Dynamic client registration (MCP requires this)
        $this->actionsCfg['register'] = ['authType' => 'none', 'checkSection' => false, 'checkOption' => false,];


        // Create PSR-17 factories
        $this->factory = new HttpFactory();

        // Create PSR-7 request from PHP globals
        $this->request = ServerRequest::fromGlobals();

        // logger
        $this->logger = new Logger();

        // cache
        $this->cache = new \Engine\Cache\Disk();

        // container
        $this->container = new Container();
        $this->container->set(\Psr\Log\LoggerInterface::class, $this->logger);
        $this->container->set(\Engine\Cache\Disk::class, $this->cache);

        // Create session store
        $this->session = new FileSessionStore(APP_REAL_PATH . $CFG['paths']['temporary'] . 'AiAssistant/McpSessions/');

        // Create HTTP transport
        $this->transport = new StreamableHttpTransport($this->request, $this->factory, $this->factory);

    }

    public function actionComplexToolSchema($R)
    {
        $this->logger->info('Starting MCP Manual Registration Server...', ['request' => new \Engine\Logger\Context\ArrayAssoc($R)]);


        // TODO SCHEMA NOT LIKE THIS!
        $schema = new \stdClass();
        $schema->type = 'object';
        $schema->required = ['title', 'date', 'type', 'attendees', ];

        $schema->properties = new \stdClass();

        // title
        $schema->properties->title = new \stdClass();
        $schema->properties->title->type = 'string';
        $schema->properties->title->description = 'the title of the event';

        // date
        $schema->properties->date = new \stdClass();
        $schema->properties->date->type = 'string';
        $schema->properties->date->description = 'the date of the event (YYYY-MM-DD)';

        // type
        $schema->properties->type = new \stdClass();
        $schema->properties->type->type = 'string';
        $schema->properties->type->description = 'the type of event';
        $schema->properties->type->enum = ['meeting', 'reminder', 'call', 'other'];

        // time
        $schema->properties->time = new \stdClass();
        $schema->properties->time->type = ['null', 'string'];
        $schema->properties->time->description = 'the time of the event (HH:MM), optional';
        $schema->properties->time->default = null;

        // priority
        $schema->properties->priority = new \stdClass();
        $schema->properties->priority->type = 'string';
        $schema->properties->priority->description = 'The priority of the event. Defaults to Normal.';
        $schema->properties->priority->default = 'normal';
        $schema->properties->priority->enum = ['low', 'normal', 'high'];

        // attendees
        $schema->properties->attendees = new \stdClass();
        $schema->properties->attendees->type = 'array';
        $schema->properties->attendees->items = new \stdClass();
        $schema->properties->attendees->items->type = 'object';
        $schema->properties->attendees->items->properties = new \stdClass();
        $schema->properties->attendees->items->properties->name = new \stdClass();
        $schema->properties->attendees->items->properties->name->type = 'string';
        $schema->properties->attendees->items->properties->email = new \stdClass();
        $schema->properties->attendees->items->properties->email->type = 'string';
        $schema->properties->attendees->items->required = ['name', 'email'];
        $schema->properties->attendees->description = 'an optional list of attendees with name and email';

        // sendInvites
        $schema->properties->sendInvites = new \stdClass();
        $schema->properties->sendInvites->type = 'boolean';
        $schema->properties->sendInvites->description = 'send calendar invites to attendees? Defaults to true if attendees are provided';
        $schema->properties->sendInvites->default = true;


        // Create MCP server
        $server = Server::builder()
            ->setServerInfo('Event Scheduler Server', '1.0.0')
            ->setLogger($this->logger)
            ->setContainer($this->container)
            ->setSession($this->session)
            //->setDiscovery(__DIR__.'/ComplexToolSchema')
            ->addTool([\plugins\AiAssistant\Mcp\Examples\ComplexToolSchema\McpEventScheduler::class, 'scheduleEvent'], 'schedule_event', 'Event scheduler', 'Schedule an event in the future.', null, json_decode(json_encode($schema), 1))
            ->build();

        $this->response = $server->run($this->transport);

        $this->logger->info('Server listener stopped gracefully.', ['response' => new \Engine\Logger\Context\StringRaw($this->response->getBody()->getContents())]);

        // Emit HTTP response
        $this->emitResponse($this->response);
    }

    public function actionCustomDependencies($R)
    {
        $this->logger->info('Starting MCP Manual Registration Server...', ['request' => new \Engine\Logger\Context\ArrayAssoc($R)]);

        $taskRepo = new \plugins\AiAssistant\Mcp\Examples\CustomDependencies\Service\InMemoryTaskRepository($this->logger);
        $this->container->set(\plugins\AiAssistant\Mcp\Examples\CustomDependencies\Service\TaskRepositoryInterface::class, $taskRepo);

        $statsService = new \plugins\AiAssistant\Mcp\Examples\CustomDependencies\Service\SystemStatsService($taskRepo);
        $this->container->set(\plugins\AiAssistant\Mcp\Examples\CustomDependencies\Service\StatsServiceInterface::class, $statsService);

        // Schema

        // Create MCP server
        $server = Server::builder()
            ->setServerInfo('Task Manager Server', '1.0.0')
            ->setLogger($this->logger)
            ->setContainer($this->container)
            ->setSession($this->session)
            ->setDiscovery(__DIR__.'/CustomDependencies')
            //->addTool([\Modules\RemoteData\Mcp\CustomDependencies\McpTaskHandlers::class, 'addTask'], 'add_task', 'Add task', 'Schedule an event in the future.', null, json_decode(json_encode($schema), 1))
            ->build();

        $this->response = $server->run($this->transport);

        $this->logger->info('Server listener stopped gracefully.', ['response' => new \Engine\Logger\Context\StringRaw($this->response->getBody()->getContents())]);

        // Emit HTTP response
        $this->emitResponse($this->response);
    }


    /**
     * GET OAuth 2.0 Protected Resource Metadata endpoint (RFC 9728).
     *
     * Advertises which authorization server(s) issue tokens for this MCP resource,
     * the supported bearer token presentation methods, and the available scopes.
     * The `resource` URL is derived from the current request URI so the response
     * is portable across environments (devel/staging/production).
     *
     * TODO [oauth-prm-discovery]: RFC 9728 expects the metadata document at the
     *   ORIGIN's `/.well-known/oauth-protected-resource` (host-root). We currently
     *   only serve it under `<mcp-base>/.well-known/oauth-protected-resource` via
     *   the per-directory .htaccess. Add a rewrite in the project root .htaccess
     *   (or vhost) so spec-strict clients can auto-discover from the origin root.
     */
    public function actionOauthProtectedResource($R)
    {
        if (strtoupper($this->request->getMethod()) !== 'GET') {
            $response = $this->factory->createResponse(405)
                ->withHeader('Allow', 'GET')
                ->withHeader('Content-Type', 'application/json');
            $response->getBody()->write((string) json_encode(['error' => 'method_not_allowed'], JSON_UNESCAPED_SLASHES));
            $this->emitResponse($response);
        }

        //$resourceUrl = $this->getMcpResourceUrl();
        $resourceUrl = 'https://mcp-dan-tina.proxy.intern.dcsplus.net';
        //$resourceUrl = 'http://php85.devel.tina.dcs/dan.baluta/tina3/project/Modules/RemoteData/Mcp/';
        //var_dump($resourceUrl); die;

        // TODO [oauth-prm-as-url]: `authorization_servers` currently points back at
        //   the MCP resource URL itself. Per RFC 9728 this field should reference a
        //   separate OAuth Authorization Server issuer. When a dedicated AS endpoint
        //   exists, replace this with that issuer URL (or list of issuers).
        $metadata = [
            'resource'                 => $resourceUrl,
            'authorization_servers'    => [$resourceUrl],
            'bearer_methods_supported' => ['header'],
            'scopes_supported'         => ['mcp:tools'],
        ];

        $this->logger->info('Serving OAuth Protected Resource Metadata.', [
            'metadata' => new \Engine\Logger\Context\ArrayAssoc($metadata),
        ]);

        $response = $this->factory->createResponse(200)
            ->withHeader('Content-Type', 'application/json')
            ->withHeader('Cache-Control', 'public, max-age=3600');

        $response->getBody()->write((string) json_encode($metadata, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE));

        $this->emitResponse($response);
    }

    /**
     * GET OAuth 2.0 Authorization Server Metadata endpoint (RFC 8414).
     *
     * Advertises the issuer, token endpoint, supported authentication methods,
     * response types, and grant types for this MCP authorization server. Uses
     * a custom API-key grant type since TINA authenticates MCP clients via the
     * `EntityMngWsAccounts` API tokens rather than a full OAuth 2.0 flow.
     *
     * TODO [oauth-as-discovery]: RFC 8414 clients look for this metadata at
     *   `/.well-known/oauth-authorization-server`. Mirror the .htaccess rewrite
     *   already in place for the protected-resource metadata so this document
     *   is auto-discoverable both at the directory-local well-known path and
     *   (ideally) at the host-root well-known path.
     */
    public function actionOauthAuthorizationServer($R)
    {
        if (strtoupper($this->request->getMethod()) !== 'GET') {
            $response = $this->factory->createResponse(405)
                ->withHeader('Allow', 'GET')
                ->withHeader('Content-Type', 'application/json');
            $response->getBody()->write((string) json_encode(['error' => 'method_not_allowed'], JSON_UNESCAPED_SLASHES));
            $this->emitResponse($response);
        }

        $issuer = $this->getMcpResourceUrl();
        $issuer = 'https://mcp-dan-tina.proxy.intern.dcsplus.net';
        //$issuer = 'http://php85.devel.tina.dcs/dan.baluta/tina3/project/Modules/RemoteData/Mcp/';

        // TODO [oauth-as-custom-grant]: `urn:ietf:params:oauth:grant-type:api_key`
        //   is NOT an IANA-registered OAuth grant type. Standard OAuth clients
        //   won't understand it and will likely expect `client_credentials` or
        //   `authorization_code`. Either register the URN, switch to a standard
        //   grant, or keep this as a documented internal contract.
        //
        // TODO [oauth-as-token-auth-method]: advertising
        //   `token_endpoint_auth_methods_supported: ["none"]` declares the token
        //   endpoint as a public client / no-auth surface. That's consistent with
        //   the api_key grant (the key itself is the credential carried in the
        //   request body), but revisit if/when the token endpoint adopts client
        //   authentication (e.g. `client_secret_basic`, `private_key_jwt`).
        //
        // TODO [oauth-as-response-types]: per RFC 8414, `response_types_supported`
        //   describes the AUTHORIZATION endpoint, not the token endpoint. With no
        //   `authorization_endpoint` field present, listing `["token"]` here may
        //   trip strict metadata validators. Either add an authorization endpoint
        //   or drop this field entirely.
        $metadata = [
            'issuer'                                => $issuer,
            'authorization_endpoint'                => $issuer . '/authorize',
            'token_endpoint'                        => $issuer . '/token',
            //'token_endpoint'                        => $issuer . '/?action=authToken',
            'registration_endpoint'                 => $issuer . '/register',
            'token_endpoint_auth_methods_supported' => ['none'],
            'response_types_supported'              => ['token','code'],
            'code_challenge_methods_supported'      => ['S256'],
            'grant_types_supported'                 => ['urn:ietf:params:oauth:grant-type:api_key'],
        ];

        $this->logger->info('Serving OAuth Authorization Server Metadata.', [
            'metadata' => new \Engine\Logger\Context\ArrayAssoc($metadata),
        ]);

        $response = $this->factory->createResponse(200)
            ->withHeader('Content-Type', 'application/json')
            ->withHeader('Cache-Control', 'public, max-age=3600');

        $response->getBody()->write((string) json_encode($metadata, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE));

        $this->emitResponse($response);
    }

    /**
     * OAuth 2.0 token endpoint (RFC 6749, section 3.2) for the custom
     * `urn:ietf:params:oauth:grant-type:api_key` grant.
     *
     * The client exchanges a long-lived TINA WS API key for an OAuth bearer
     * access token. Since the API key is itself the credential the rest of
     * the MCP stack expects, we simply echo it back as the `access_token`
     * value — there is no separate token minting / persistence step.
     *
     * Accepted request shapes:
     *   POST application/x-www-form-urlencoded   (canonical OAuth)
     *   POST application/json                    (lenient)
     *   GET  with `api_key` query parameter      (lenient, debug-friendly)
     *
     * Recognised parameters:
     *   api_key     (required)
     *   grant_type  (optional; when present must equal the api_key grant URN)
     *
     * TODO [authtoken-no-real-issuance]: the response echoes the api_key back as
     *   `access_token`, so there is no separation between the presented credential
     *   and the issued token. Any leak (logs, response body, browser history) leaks
     *   the long-lived credential itself. Replace with short-lived opaque tokens
     *   bound to the api_key (e.g. persisted in a sessions/tokens store) before
     *   exposing this endpoint outside trusted networks.
     */
    public function actionAuthToken($R)
    {
        $method = strtoupper($this->request->getMethod());

        // TODO [authtoken-strict-method]: GET is accepted here purely for manual
        //   debugging / curl. RFC 6749 § 3.2 mandates POST and forbids placing
        //   credentials in URLs (they leak into access logs and HTTP Referer
        //   headers). Remove 'GET' from the allow-list (and from the Allow header)
        //   once external clients are wired up.
        if (!in_array($method, ['GET', 'POST'], true)) {
            $response = $this->factory->createResponse(405)
                ->withHeader('Allow', 'GET, POST')
                ->withHeader('Content-Type', 'application/json');
            $response->getBody()->write((string) json_encode(['error' => 'method_not_allowed'], JSON_UNESCAPED_SLASHES));
            $this->emitResponse($response);
        }

        $params = $this->collectTokenRequestParams();

        $grantType = isset($params['grant_type']) ? (string) $params['grant_type'] : null;
        $apiKey    = isset($params['api_key']) ? trim((string) $params['api_key']) : '';

        // TODO [authtoken-require-grant-type]: `grant_type` is optional today for
        //   convenience. RFC 6749 makes it REQUIRED. Tighten this check to reject
        //   requests where `$grantType === null` (or change the condition to
        //   `$grantType !== '<urn>'`) once clients are aware of the grant URN.
        if ($grantType !== null && $grantType !== 'urn:ietf:params:oauth:grant-type:api_key') {
            $this->emitOauthTokenError('unsupported_grant_type', 'Only the api_key grant type is supported.', 400);
        }

        if ($apiKey === '') {
            $this->emitOauthTokenError('invalid_request', 'Missing required parameter: api_key.', 400);
        }

        if (!$this->isValidApiKey($apiKey)) {
            $this->logger->warning('Token endpoint rejected unknown api_key.', [
                'method' => $method,
            ]);
            $this->emitOauthTokenError('invalid_grant', 'The provided api_key is not recognised.', 401);
        }

        // TODO [authtoken-response-fields]: the response intentionally omits
        //   `expires_in` and `scope`. RFC 6749 § 5.1 makes them OPTIONAL but most
        //   clients expect at least `expires_in`. When real token rotation is
        //   introduced (see authtoken-no-real-issuance), add `expires_in`,
        //   `scope`, and consider `refresh_token` here.
        $payload = [
            'access_token' => $apiKey,
            'token_type'   => 'Bearer',
        ];

        $this->logger->info('Token endpoint issued bearer token for valid api_key.', [
            'method' => $method,
        ]);

        $response = $this->factory->createResponse(200)
            ->withHeader('Content-Type', 'application/json')
            ->withHeader('Cache-Control', 'no-store')
            ->withHeader('Pragma', 'no-cache');

        $response->getBody()->write((string) json_encode($payload, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE));

        $this->emitResponse($response);
    }

    /**
     * /authorize endpoint
     *
     * Standard OAuth 2.0 Authorization Code flow with PKCE (Proof Key for Code Exchange)
     * Step 1 — Authenticate the user (show a login page, check session, etc.)
     * Step 2 — On success, issue an auth code and redirect the browser to the redirect_uri with an authorization code and the same state
     *
     * Important points:
     *
     * code should be a short-lived authorization code generated by your server.
     * Store the code_challenge, code_challenge_method, client_id, redirect_uri, and user/session info against that code.
     * Later, when /token is called, validate the submitted code_verifier against the stored code_challenge.
     * Always echo back state unchanged.
     * If there is an error, redirect with error and state
     */
    public function actionAuthorize($R)
    {
        \Log::getInstance()->info('/authorize endpoint called', ['request' => new \Engine\Logger\Context\ArrayAssoc($R)]);

        $redirect_uri = $R['redirect_uri'].'?code=wWGQd70zTiQvGjEoE7qelUqV7wmqORu4vLE&state='.$R['state'];
        //var_dump($R, $redirect_uri); die;

        return new \Engine\Mvc\Response\HtmlRedirect($redirect_uri);
    }

    /**
     * /token endppoint
     *
     * 1. Look up `code` in your store → must exist and not be expired
     * 2. Check `client_id` matches what's stored with the code
     * 3. Check `redirect_uri` matches exactly what's stored
     * 4. Verify PKCE: BASE64URL(SHA256(code_verifier)) === stored code_challenge
     * 5. Invalidate the code immediately (single-use)
     */
    public function actionToken($R)
    {
        \Log::getInstance()->info('/token endpoint called', ['request' => new \Engine\Logger\Context\ArrayAssoc($R)]);

        $body = [
            'access_token'      => 'wWGQd70zTiQvGjEoE7qelUqV7wmqORu4vLE',
            'token_type'        => 'Bearer',
            //'expires_in'        => 3600,
            //'refresh_token'   => '<optional but recommended>',
        ];

        $response = $this->factory->createResponse(200)
            ->withHeader('Content-Type', 'application/json')
            ->withHeader('Cache-Control', 'no-store')
            ->withHeader('Pragma', 'no-cache');

        $response->getBody()->write((string) json_encode($body, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE));

        $this->emitResponse($response);
    }

    /**
     * /register endpoint
     *
     *  An empty hit on /register usually means the client is attempting OAuth Dynamic Client Registration (RFC 7591).
     *
     * Even if the request body is empty, still generate and return a client_id.
     * client_secret can be omitted for public clients.
     * Return:
     * 201 Created
     * Content-Type: application/json
     *
     */
    public function actionRegister($R)
    {
        \Log::getInstance()->info('/register endpoint called', ['request' => new \Engine\Logger\Context\ArrayAssoc($R)]);

        $body = [
            'client_id'                     => uniqid('TINA_'),
            //'client_secret'                 => null,
            'client_id_issued_at'           => TIMESTAMP,
            'client_secret_expires_at'      => 0,
            //'client_name'       => 'TINA MCP Client',
            'grant_types'                   => ['authorization_code'],
            'response_types'                => ['code'],
            'token_endpoint_auth_method'    => 'none',
            'redirect_uris'     => [],
        ];

        $response = $this->factory->createResponse(201)
            ->withHeader('Content-Type', 'application/json')
            ->withHeader('Cache-Control', 'no-store')
            ->withHeader('Pragma', 'no-cache');

        $response->getBody()->write((string) json_encode($body, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE));

        $this->emitResponse($response);
    }

    /**
     * Collects token-endpoint parameters from any of the supported request
     * shapes (form-encoded body, JSON body, query string). Body parameters
     * take precedence over query parameters per RFC 6749.
     *
     * @return array<string, mixed>
     */
    private function collectTokenRequestParams(): array
    {
        $params = [];

        $queryParams = $this->request->getQueryParams();
        if (is_array($queryParams)) {
            $params = $queryParams;
        }

        $parsedBody = $this->request->getParsedBody();
        if (is_array($parsedBody)) {
            $params = array_merge($params, $parsedBody);
        }
        else {
            $rawBody = (string) $this->request->getBody();
            if ($rawBody !== '') {
                $decoded = json_decode($rawBody, true);
                if (is_array($decoded)) {
                    $params = array_merge($params, $decoded);
                }
            }
        }

        return $params;
    }

    /**
     * Emits an OAuth 2.0 token-endpoint error response per RFC 6749 § 5.2 and
     * terminates the request.
     */
    private function emitOauthTokenError(string $errorCode, string $description, int $httpStatus): never
    {
        $body = [
            'error'             => $errorCode,
            'error_description' => $description,
        ];

        $response = $this->factory->createResponse($httpStatus)
            ->withHeader('Content-Type', 'application/json')
            ->withHeader('Cache-Control', 'no-store')
            ->withHeader('Pragma', 'no-cache');

        $response->getBody()->write((string) json_encode($body, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE));

        $this->emitResponse($response);
    }

    /**
     * Checks whether the provided token matches an active TINA WS API key.
     * Pure validation: no side-effects, no response emission.
     */
    private function isValidApiKey(?string $providedToken): bool
    {
        if ($providedToken === null || $providedToken === '') {
            return false;
        }

        $entityObj = new \App\EntityList\EntityMngWsAccounts();
        $entityObj->setFilter('authToken', '=', $providedToken);
        $entityObj->setFilter('blocked', '=', 0);
        $records = $entityObj->exportRecords();

        return count($records) === 1;
    }

    /**
     * Builds the canonical MCP resource URL (scheme + host[:port] + path) from
     * the current request URI, stripping any query string. The path is kept
     * as-is and guaranteed to end with a trailing slash to match the directory
     * style endpoint expected by the OAuth Protected Resource Metadata spec.
     */
    private function getMcpResourceUrl(): string
    {
        $uri = $this->request->getUri();

        $url = $uri->getScheme() . '://' . $uri->getHost();

        $port = $uri->getPort();
        if ($port !== null && !in_array($port, [80, 443], true)) {
            $url .= ':' . $port;
        }

        $path = $uri->getPath();
        if ($path === '' || substr($path, -1) !== '/') {
            $path .= '/';
        }

        return $url . $path;
    }


    // TODO DON'T EXPOSE BUSINESS LOGIC, USE DIFFERENT TOOLS

    // THIS AUTHENTICATION DOES NOT WORK AUTOMATICALLY IN ALL AGENTS, NEEDS OAUTH2:
    //  TODO @see https://developers.openai.com/codex/mcp
    //  TOOD @see https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization

    # Agents (Claude, Codex, Cursor etc.) setup with Bearer token:
    # mcp add --transport http TINA http://php85.devel.tina.dcs/dan.baluta/tina3/project/Modules/RemoteData/Mcp/ --header "Authorization: Bearer wWGQd70zTiQvGjEoE7qelUqV7wmqORu4vLE"

    public function actionIndex($R)
    {
        $this->logger->info('Starting MCP Manual Registration Server...', ['request' => new \Engine\Logger\Context\ArrayAssoc($R)]);

        // AUTHENTICATE (WS API KEY)
        $authorizationHeader = $this->request->getHeaderLine('Authorization');

        $providedToken = null;
        if (preg_match('/^Bearer\s+(.+)$/i', $authorizationHeader, $matches)) {
            $providedToken = trim($matches[1]);
        }

        if (!$this->authenticateBearerToken($providedToken)) {
            return;
        }

        $suppliersContext = new \Plugins\AiAssistant\Contexts\ReportsData\Suppliers();
        $this->container->set(\Plugins\AiAssistant\Contexts\ReportsData\Suppliers::class, $suppliersContext);

        $supplierContext = new \Plugins\AiAssistant\Contexts\Entity\Supplier();
        $this->container->set(\Plugins\AiAssistant\Contexts\Entity\Supplier::class, $supplierContext);

        $corporatesContext = new \Plugins\AiAssistant\Contexts\ReportsData\Corporates();
        $this->container->set(\Plugins\AiAssistant\Contexts\ReportsData\Corporates::class, $corporatesContext);

        $corporateContext = new \Plugins\AiAssistant\Contexts\Entity\Corporate();
        $this->container->set(\Plugins\AiAssistant\Contexts\Entity\Corporate::class, $corporateContext);

        $customersContext = new \Plugins\AiAssistant\Contexts\ReportsData\Customers();
        $this->container->set(\Plugins\AiAssistant\Contexts\ReportsData\Customers::class, $customersContext);

        $customerContext = new \Plugins\AiAssistant\Contexts\Entity\Customer();
        $this->container->set(\Plugins\AiAssistant\Contexts\Entity\Customer::class, $customerContext);

        $contactPersonsContext = new \Plugins\AiAssistant\Contexts\ReportsData\ContactPersons();
        $this->container->set(\Plugins\AiAssistant\Contexts\ReportsData\ContactPersons::class, $contactPersonsContext);

        $contactPersonContext = new \Plugins\AiAssistant\Contexts\Entity\ContactPerson();
        $this->container->set(\Plugins\AiAssistant\Contexts\Entity\ContactPerson::class, $contactPersonContext);

        $trustsContext = new \Plugins\AiAssistant\Contexts\ReportsData\Trusts();
        $this->container->set(\Plugins\AiAssistant\Contexts\ReportsData\Trusts::class, $trustsContext);

        $corporateOfficesContext = new \Plugins\AiAssistant\Contexts\ReportsData\CorporateOffices();
        $this->container->set(\Plugins\AiAssistant\Contexts\ReportsData\CorporateOffices::class, $corporateOfficesContext);

        $corporateOfficeContext = new \Plugins\AiAssistant\Contexts\Entity\CorporateOffice();
        $this->container->set(\Plugins\AiAssistant\Contexts\Entity\CorporateOffice::class, $corporateOfficeContext);


        // Create MCP server
        $server = Server::builder()
            ->setServerInfo('TINA MCP - Enterprise travel ERP and mid-backoffice solution', '1.0.0')
            ->setProtocolVersion(ProtocolVersion::V2025_11_25)
            ->setLogger($this->logger)
            ->setContainer($this->container)
            ->setSession($this->session)

            // suppliers
            ->addTool([\Modules\RemoteData\Mcp\ReportsData\Suppliers::class, 'listSuppliers'], 'list_suppliers', 'List suppliers', 'Run aggregations and filters on suppliers database table.', null, $suppliersContext->generateMcpInputSchema('run_rd_suppliers_analytics'))
            ->addTool([\Modules\RemoteData\Mcp\Entity\Supplier::class, 'createSupplierDraft'], 'create_supplier_draft', 'Create a new supplier draft', 'Create a new supplier draft.', null, $supplierContext->generateMcpInputSchema('create_supplier_draft'))
            ->addTool([\Modules\RemoteData\Mcp\Entity\Supplier::class, 'updateSupplierDraft'], 'update_supplier_draft', 'Update supplier draft', 'Update an existing supplier draft with new or corrected fields.', null, $supplierContext->generateMcpInputSchema('update_supplier_draft'))
            ->addTool([\Modules\RemoteData\Mcp\Entity\Supplier::class, 'previewSupplier'], 'preview_supplier', 'Preview supplier', 'Preview the supplier draft before submission.', null, $supplierContext->generateMcpInputSchema('preview_supplier'))
            ->addTool([\Modules\RemoteData\Mcp\Entity\Supplier::class, 'submitSupplier'], 'submit_supplier', 'Submit supplier', 'Commit/post the supplier draft into the system', null, $supplierContext->generateMcpInputSchema('submit_supplier'))

            // corporates
            ->addTool([\Modules\RemoteData\Mcp\ReportsData\Corporates::class, 'listCorporates'], 'list_corporates', 'List corporates', 'Run aggregations and filters on corporates (companies) database table.', null, $corporatesContext->generateMcpInputSchema('run_rd_corporates_analytics'))
            ->addTool([\Modules\RemoteData\Mcp\Entity\Corporate::class, 'createCorporateDraft'], 'create_corporate_draft', 'Create a new corporate draft', 'Create a new corporate/company draft.', null, $corporateContext->generateMcpInputSchema('create_corporate_draft'))
            ->addTool([\Modules\RemoteData\Mcp\Entity\Corporate::class, 'updateCorporateDraft'], 'update_corporate_draft', 'Update corporate draft', 'Update an existing corporate draft with new or corrected fields.', null, $corporateContext->generateMcpInputSchema('update_corporate_draft'))
            ->addTool([\Modules\RemoteData\Mcp\Entity\Corporate::class, 'previewCorporate'], 'preview_corporate', 'Preview corporate', 'Preview the corporate draft before submission.', null, $corporateContext->generateMcpInputSchema('preview_corporate'))
            ->addTool([\Modules\RemoteData\Mcp\Entity\Corporate::class, 'submitCorporate'], 'submit_corporate', 'Submit corporate', 'Commit/post the corporate draft into the system', null, $corporateContext->generateMcpInputSchema('submit_corporate'))

            // customers
            ->addTool([\Modules\RemoteData\Mcp\ReportsData\Customers::class, 'listCustomers'], 'list_customers', 'List customers', 'Run aggregations and filters on customers (passengers/individuals) database table.', null, $customersContext->generateMcpInputSchema('run_rd_customers_analytics'))
            ->addTool([\Modules\RemoteData\Mcp\Entity\Customer::class, 'createCustomerDraft'], 'create_customer_draft', 'Create a new customer draft', 'Create a new customer/passenger/individual draft.', null, $customerContext->generateMcpInputSchema('create_customer_draft'))
            ->addTool([\Modules\RemoteData\Mcp\Entity\Customer::class, 'updateCustomerDraft'], 'update_customer_draft', 'Update customer draft', 'Update an existing customer draft with new or corrected fields.', null, $customerContext->generateMcpInputSchema('update_customer_draft'))
            ->addTool([\Modules\RemoteData\Mcp\Entity\Customer::class, 'previewCustomer'], 'preview_customer', 'Preview customer', 'Preview the customer draft before submission.', null, $customerContext->generateMcpInputSchema('preview_customer'))
            ->addTool([\Modules\RemoteData\Mcp\Entity\Customer::class, 'submitCustomer'], 'submit_customer', 'Submit customer', 'Commit/post the customer draft into the system', null, $customerContext->generateMcpInputSchema('submit_customer'))

            // contact persons
            ->addTool([\Modules\RemoteData\Mcp\ReportsData\ContactPersons::class, 'listContactPersons'], 'list_contact_persons', 'List contact persons', 'Run aggregations and filters on contact persons database table.', null, $contactPersonsContext->generateMcpInputSchema('run_rd_contactPersons_analytics'))
            ->addTool([\Modules\RemoteData\Mcp\Entity\ContactPerson::class, 'createContactPersonDraft'], 'create_contact_person_draft', 'Create a new contact person draft', 'Create a new contact person draft.', null, $contactPersonContext->generateMcpInputSchema('create_contact_person_draft'))
            ->addTool([\Modules\RemoteData\Mcp\Entity\ContactPerson::class, 'updateContactPersonDraft'], 'update_contact_person_draft', 'Update contact person draft', 'Update an existing contact person draft with new or corrected fields.', null, $contactPersonContext->generateMcpInputSchema('update_contact_person_draft'))
            ->addTool([\Modules\RemoteData\Mcp\Entity\ContactPerson::class, 'previewContactPerson'], 'preview_contact_person', 'Preview contact person', 'Preview the contact person draft before submission.', null, $contactPersonContext->generateMcpInputSchema('preview_contact_person'))
            ->addTool([\Modules\RemoteData\Mcp\Entity\ContactPerson::class, 'submitContactPerson'], 'submit_contact_person', 'Submit contact person', 'Commit/post the contact person draft into the system', null, $contactPersonContext->generateMcpInputSchema('submit_contact_person'))

            // trusts (holdings)
            ->addTool([\Modules\RemoteData\Mcp\ReportsData\Trusts::class, 'listTrusts'], 'list_trusts', 'List trusts/holdings', 'Run aggregations and filters on trusts (holdings) database table. Corporations are grouped under holdings.', null, $trustsContext->generateMcpInputSchema('run_rd_trusts_analytics'))

            // corporate offices
            ->addTool([\Modules\RemoteData\Mcp\ReportsData\CorporateOffices::class, 'listCorporateOffices'], 'list_corporate_offices', 'List corporate offices', 'Run aggregations and filters on corporate offices database table.', null, $corporateOfficesContext->generateMcpInputSchema('run_rd_corporateOffices_analytics'))
            ->addTool([\Modules\RemoteData\Mcp\Entity\CorporateOffice::class, 'createCorporateOfficeDraft'], 'create_corporate_office_draft', 'Create a new corporate office draft', 'Create a new corporate/company office draft.', null, $corporateOfficeContext->generateMcpInputSchema('create_corporate_office_draft'))
            ->addTool([\Modules\RemoteData\Mcp\Entity\CorporateOffice::class, 'updateCorporateOfficeDraft'], 'update_corporate_office_draft', 'Update corporate office draft', 'Update an existing corporate office draft with new or corrected fields.', null, $corporateOfficeContext->generateMcpInputSchema('update_corporate_office_draft'))
            ->addTool([\Modules\RemoteData\Mcp\Entity\CorporateOffice::class, 'previewCorporateOffice'], 'preview_corporate_office', 'Preview corporate office', 'Preview the corporate office draft before submission.', null, $corporateOfficeContext->generateMcpInputSchema('preview_corporate_office'))
            ->addTool([\Modules\RemoteData\Mcp\Entity\CorporateOffice::class, 'submitCorporateOffice'], 'submit_corporate_office', 'Submit corporate office', 'Commit/post the corporate office draft into the system', null, $corporateOfficeContext->generateMcpInputSchema('submit_corporate_office'))

            // resources
            ->addResource([\Modules\RemoteData\Mcp\Resources\Application::class, 'getAppVersion'], 'app://version')
//          ->addResourceTemplate('current_time', 'Current time', 'Get the current server time in ISO 8601 format.', function() {
//              return (new \DateTime())->format(DATE_ATOM);
//          })

            // prompts
            ->addPrompt([\Modules\RemoteData\Mcp\Prompts\System::class, 'systemPrompt'], 'system_prompt')

            ->build();

        $this->response = $server->run($this->transport);

        $this->logger->info('Server listener stopped gracefully.', ['response' => new \Engine\Logger\Context\StringRaw($this->response->getBody()->getContents())]);

        // Emit HTTP response
        $this->emitResponse($this->response);
    }


    /**
     * Validates the Bearer token from the Authorization header against the expected value.
     * On failure, emits a JSON-RPC -32001 error response (HTTP 401) and terminates the request.
     *
     * @param string $providedToken The provided Bearer token value.
     * @return bool True when the request is authorized (only returns on success; on failure it exits via emitResponse).
     */
    private function authenticateBearerToken(?string $providedToken): bool
    {
        if ($this->isValidApiKey($providedToken)) {
            return true;
        }

        // TODO check this
        $requestId = $this->peekJsonRpcRequestId();
        
        $error = new \Mcp\Schema\JsonRpc\Error(
            $requestId,
            -32001,
            'Unauthorized: invalid or missing API key',
        );

        $this->logger->warning('Unauthorized MCP request: invalid or missing Bearer token.', [
            'authorizationHeaderPresent' => $providedToken !== null,
        ]);

        $errorResponse = $this->factory->createResponse(401)
            ->withHeader('Content-Type', 'application/json')
            // TODO IMPLEMENT PROPER MCP OAUTH: https://modelcontextprotocol.io/docs/tutorials/security/authorization
            //->withHeader('WWW-Authenticate', 'Bearer realm="mcp", resource_metadata="http://php85.devel.tina.dcs/dan.baluta/tina3/project/plugins/AiAssistant/Mcp/?action=oauthProtectedResource"')
        ;

        $errorResponse->getBody()->write((string) json_encode($error, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE));

        $this->emitResponse($errorResponse);
    }

    /**
     * Best-effort extraction of the JSON-RPC request id from the request body so the
     * error response can echo it back per JSON-RPC 2.0. Falls back to '' when unavailable.
     */
    private function peekJsonRpcRequestId(): string|int
    {
        try {
            $body = (string) $this->request->getBody();
        } catch (\Throwable) {
            return '';
        }

        if ('' === $body) {
            return '';
        }

        $decoded = json_decode($body, true);
        if (is_array($decoded) && isset($decoded['id']) && (is_string($decoded['id']) || is_int($decoded['id']))) {
            return $decoded['id'];
        }

        return '';
    }
    

    private function emitResponse(\Psr\Http\Message\ResponseInterface|int $response): never
    {
        if ('cli' === \PHP_SAPI) {
            exit($response);
        }

        // TODO
        //(new SapiEmitter())->emit($result);

        http_response_code($response->getStatusCode());

        foreach ($response->getHeaders() as $name => $values) {
            foreach ($values as $value) {
                header("$name: $value", false);
            }
        }

        echo $response->getBody();
        exit(0);
    }

}
