<?php

declare(strict_types=1);

namespace Plugin\tnt_mcp\src\Ai;

/**
 * What one completion call came back with (epic 4.4), in the shape everything downstream reads.
 *
 * ⚠️ `error` is always a message THIS plugin wrote — never the provider's response body. The body is
 * attacker-influenced text that ends up in the execution log, in an admin page and in an MCP result,
 * and some proxies echo the request headers (which is to say the API key) into their error bodies.
 * The status code carries everything a merchant can act on; {@see AiChatClient} maps it to a
 * sentence. The same rule as {@see AiConnectionTester}, for the same reason.
 *
 * Token counts are carried even though nothing bills against them yet: they come back in the same
 * response, and phase 4.5's cost logging must not have to make a second call to learn them. They are
 * 0 when the provider did not report usage — some OpenAI-compatible runtimes do not.
 */
final class AiChatResult implements \JsonSerializable
{
    /**
     * @param bool   $ok               whether there is a usable answer in `$text`
     * @param string $text             the model's answer, '' on failure
     * @param string $error            why it failed, '' on success — our words, never the provider's
     * @param string $model            the model actually used, as the provider reported it, or as asked
     * @param int    $status           the HTTP status of the last attempt, 0 when nothing answered
     * @param int    $promptTokens     input tokens the provider billed, 0 when not reported
     * @param int    $completionTokens output tokens the provider billed, 0 when not reported
     * @param int    $attempts         how many requests were sent, including the successful one
     * @param mixed  $json             the answer decoded, when JSON output was asked for; else null
     */
    public function __construct(
        public readonly bool $ok,
        public readonly string $text = '',
        public readonly string $error = '',
        public readonly string $model = '',
        public readonly int $status = 0,
        public readonly int $promptTokens = 0,
        public readonly int $completionTokens = 0,
        public readonly int $attempts = 0,
        public readonly mixed $json = null,
    ) {
    }

    public static function failed(
        string $error,
        int $status = 0,
        int $attempts = 0,
        string $model = '',
    ): self {
        return new self(false, '', $error, $model, $status, 0, 0, $attempts);
    }

    /** Tokens billed for this call. 0 when the provider reported no usage at all. */
    public function totalTokens(): int
    {
        return $this->promptTokens + $this->completionTokens;
    }

    /**
     * @return array{
     *     ok: bool,
     *     text: string,
     *     error: string,
     *     model: string,
     *     status: int,
     *     promptTokens: int,
     *     completionTokens: int,
     *     totalTokens: int,
     *     attempts: int,
     *     json: mixed
     * }
     */
    public function jsonSerialize(): array
    {
        return [
            'ok'               => $this->ok,
            'text'             => $this->text,
            'error'            => $this->error,
            'model'            => $this->model,
            'status'           => $this->status,
            'promptTokens'     => $this->promptTokens,
            'completionTokens' => $this->completionTokens,
            'totalTokens'      => $this->totalTokens(),
            'attempts'         => $this->attempts,
            'json'             => $this->json,
        ];
    }
}
