<?php

declare(strict_types=1);

namespace Plugin\tnt_mcp\src\Mcp\Tools;

use Plugin\tnt_mcp\src\Mcp\Contracts\Tool;
use Plugin\tnt_mcp\src\Mcp\Mode\EndpointClass;
use Plugin\tnt_mcp\src\Ai\AiBudget;
use Plugin\tnt_mcp\src\Ai\AiPricing;
use Plugin\tnt_mcp\src\Ai\AiUsageRepository;

/**
 * READ tool: what an AI workflow step would cost, and what the shop has left today (epic 4.3/4.5).
 *
 * It was deliberately NOT built in phase 4.3, because a cost estimator with no prices and no spend
 * counters behind it can only guess. Phase 4.5 built both, so this tool now answers with the same
 * numbers the budget guard enforces — an estimate that disagreed with the guard would be worse than
 * no estimate at all.
 *
 * ## Why the budget comes back too
 *
 * The client's real question is never "what does 1000 tokens cost?" — it is "may I build a workflow
 * that does this a hundred times a day on this shop?". Answering only the first half would have the
 * client confidently author a workflow the guard then refuses every afternoon. The budget fields
 * make the second half answerable in the same call.
 *
 * ⚠️ The answer is an ESTIMATE from list prices shipped with this build. It is not a quote, and the
 * provider's invoice is the only authority; {@see AiPricing} says so in more detail. A model this
 * build has no price for returns `costKnown: false` with a hint naming the override setting, rather
 * than a confident zero.
 */
final class EstimatePromptCost implements Tool
{
    /** @var \Closure(): AiBudget */
    private readonly \Closure $budget;

    /** @var \Closure(): AiUsageRepository */
    private readonly \Closure $usage;

    /**
     * Both closures are lazy for the same reason every other tool's are: the tool list is built in
     * contexts with no shop container, so a constructor that reached for the database would make
     * `tools/list` fail rather than this one tool.
     *
     * @param (\Closure(): AiBudget)|null           $budget
     * @param (\Closure(): AiUsageRepository)|null  $usage
     */
    public function __construct(?\Closure $budget = null, ?\Closure $usage = null)
    {
        $this->budget = $budget ?? static fn (): AiBudget => AiBudget::fromSettings();
        $this->usage  = $usage ?? static fn (): AiUsageRepository => new AiUsageRepository();
    }

    public function name(): string
    {
        return 'estimate_prompt_cost';
    }

    public function description(): string
    {
        return 'Estimate what an AI workflow step costs in USD for a model and token counts, and '
            . 'how much of the shop\'s daily AI budget is left.';
    }

    /** @return array<string,mixed> */
    public function inputSchema(): array
    {
        return [
            'type'       => 'object',
            'properties' => [
                'model' => [
                    'type'        => 'string',
                    'description' => 'Model id, e.g. claude-opus-5 or gpt-4o-mini. '
                        . 'Leave empty to price the shop\'s configured default.',
                ],
                'inputTokens' => [
                    'type'        => 'integer',
                    'minimum'     => 0,
                    'description' => 'Prompt tokens per call. Roughly one token per four characters '
                        . 'of English text.',
                ],
                'outputTokens' => [
                    'type'        => 'integer',
                    'minimum'     => 0,
                    'description' => 'Answer tokens per call. Usually the action\'s maxTokens.',
                ],
                'callsPerDay' => [
                    'type'        => 'integer',
                    'minimum'     => 1,
                    'description' => 'How often the workflow would run per day. Defaults to 1.',
                ],
            ],
            'required'             => ['inputTokens', 'outputTokens'],
            'additionalProperties' => false,
        ];
    }

    public function isWrite(): bool
    {
        return false;
    }

    public function scope(): string
    {
        return 'ai:read';
    }

    public function endpointClass(): EndpointClass
    {
        return EndpointClass::Reversible;
    }

    /**
     * @param array<string,mixed> $args
     *
     * @return array<string,mixed>
     */
    public function run(array $args): array
    {
        $model   = \trim((string)($args['model'] ?? ''));
        $input   = \max(0, (int)($args['inputTokens'] ?? 0));
        $output  = \max(0, (int)($args['outputTokens'] ?? 0));
        $perDay  = \max(1, (int)($args['callsPerDay'] ?? 1));
        $budget  = ($this->budget)();
        $perCall = $budget->pricing->costMicros($model, $input, $output);

        if ($perCall === null) {
            return [
                'ok'        => true,
                'model'     => $model,
                'costKnown' => false,
                'hint'      => 'No price is on file for this model, so its calls cannot be counted '
                    . 'against the daily spending limit. An admin can supply one under Plugins → '
                    . 'JTL Shop - AI Connector → KI as "input/output" USD per 1M tokens, or the shop '
                    . 'can bound these calls with the runs-per-hour limit instead.',
            ] + $this->budgetState($budget);
        }

        return [
            'ok'             => true,
            'model'          => $model,
            'costKnown'      => true,
            'costPerCallUsd' => AiBudget::formatUsd($perCall),
            'costPerDayUsd'  => AiBudget::formatUsd($perCall * $perDay),
            'callsPerDay'    => $perDay,
            'inputTokens'    => $input,
            'outputTokens'   => $output,
            'hint'           => 'Estimate from list prices shipped with this plugin, not a quote — '
                . 'the provider\'s invoice is what the shop actually pays.',
        ] + $this->budgetState($budget);
    }

    /**
     * What the shop has already spent and what it allows.
     *
     * Reads the counters only when a limit is actually set — the same rule the guard follows, so
     * that a shop with no budget pays no query for asking about one.
     *
     * @return array<string,mixed>
     */
    private function budgetState(AiBudget $budget): array
    {
        if (!$budget->hasSpendLimit() && !$budget->hasRunLimit()) {
            return [
                'dailyLimitUsd'  => '',
                'hourlyCallCap'  => 0,
                'budgetHint'     => 'This shop sets no AI spending or rate limit, so nothing here '
                    . 'refuses a call. Limits live under Plugins → JTL Shop - AI Connector → KI.',
            ];
        }

        $usage = ($this->usage)();
        $spent = $budget->hasSpendLimit() ? $usage->spentTodayMicros() : 0;

        return [
            'dailyLimitUsd' => $budget->hasSpendLimit()
                ? AiBudget::formatUsd($budget->maxSpendPerDayMicros)
                : '',
            'spentTodayUsd' => $budget->hasSpendLimit() ? AiBudget::formatUsd($spent) : '',
            'remainingTodayUsd' => $budget->hasSpendLimit()
                ? AiBudget::formatUsd(\max(0, $budget->maxSpendPerDayMicros - $spent))
                : '',
            'hourlyCallCap'  => $budget->maxRunsPerHour,
            'callsThisHour'  => $budget->hasRunLimit() ? $usage->runsThisHour() : 0,
            'budgetHint'     => 'A call is refused once a limit is reached; the reason is written to '
                . 'the workflow ExecutionLog.',
        ];
    }
}
