<?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 Mcp\Server\Stateless;

use Mcp\Schema\JsonRpc\Error;
use Mcp\Schema\JsonRpc\MessageInterface;

/**
 * One modern-era answer, paired with the HTTP status that carries it.
 *
 * SEP-2575 fixes specific statuses to specific JSON-RPC error codes, so the
 * status is part of the answer rather than something the transport re-derives.
 *
 * @author Christopher Hertel <mail@christopher-hertel.de>
 */
final class StatelessResult
{
    /**
     * @param (\Closure(): \Generator<mixed>)|null $frames set instead of $message when the answer is a stream
     * @param array<string, mixed>|null            $body   a result body already through the wire codec
     */
    private function __construct(
        public readonly ?MessageInterface $message,
        public readonly int $httpStatus,
        public readonly ?\Closure $frames = null,
        private readonly ?array $body = null,
        private readonly string|int|null $id = null,
        private readonly bool $bodyless = false,
    ) {
    }

    /**
     * A successful answer whose body the wire codec has already stamped — a
     * plain array, since no result class models the fields it added.
     *
     * @param array<string, mixed> $body
     */
    public static function ok(string|int $id, array $body): self
    {
        return new self(null, 200, null, $body, $id);
    }

    public static function error(Error $error, int $httpStatus): self
    {
        return new self($error, $httpStatus);
    }

    /**
     * A status with no body — what a notification gets, since it has no id to
     * correlate a JSON-RPC message against.
     */
    public static function empty(int $httpStatus): self
    {
        return new self(null, $httpStatus, bodyless: true);
    }

    /**
     * A long-lived answer delivered as frames — today only
     * `subscriptions/listen`. Deferred, since the frames are produced over the
     * life of the connection.
     *
     * @param \Closure(): \Generator<mixed> $frames
     */
    public static function stream(\Closure $frames): self
    {
        return new self(null, 200, $frames);
    }

    public function isStream(): bool
    {
        return null !== $this->frames;
    }

    public function isEmpty(): bool
    {
        return $this->bodyless;
    }

    public function isError(): bool
    {
        return $this->message instanceof Error;
    }

    public function toJson(): string
    {
        if (null !== $this->body) {
            return json_encode([
                'jsonrpc' => MessageInterface::JSONRPC_VERSION,
                'id' => $this->id,
                'result' => $this->body,
            ], \JSON_THROW_ON_ERROR | \JSON_UNESCAPED_SLASHES);
        }

        if ($this->bodyless) {
            throw new \LogicException('This result carries no body; send its status alone.');
        }

        if (null === $this->message) {
            throw new \LogicException('A streaming or empty result has no single JSON body; check isStream()/isEmpty() first.');
        }

        return json_encode($this->message, \JSON_THROW_ON_ERROR | \JSON_UNESCAPED_SLASHES);
    }
}