<?php

declare(strict_types=1);

namespace Plugin\tnt_mcp\src\Ai;

use Plugin\tnt_mcp\src\Enums\AiProviders;

/**
 * The shop's LLM provider settings as everything OUTSIDE {@see AiConfigurationStore} is allowed to
 * see them (epic phase 4.2).
 *
 * ⚠️ There is no API key on this object, and there must never be one. The key leaves the store
 * through exactly one method — `AiConfigurationStore::apiKey()` — which is called by the HTTP lane
 * and by nothing else. Everything that reports state (the MCP tools of phase 4.3, the admin tab,
 * the execution log) reads THIS object, so no amount of new callers can start leaking the secret:
 * the type simply does not carry it.
 *
 * What it carries instead is {@see self::$apiKeyLast4} — enough for a merchant to recognise WHICH
 * key is stored ("…4f2a, yes, the one from the billing account") without it being a credential.
 * Four characters of a ~48-character random string identify nothing on their own.
 */
final class AiConfiguration implements \JsonSerializable
{
    public function __construct(
        public readonly ?AiProviders $provider = null,
        public readonly string $model = '',
        /** Merchant-supplied API root; '' means "use the provider default". */
        public readonly string $baseUrl = '',
        public readonly bool $apiKeyConfigured = false,
        /** Last four characters of the stored key, or '' when none is stored. */
        public readonly string $apiKeyLast4 = '',
        /** When the configuration was last written, 'Y-m-d H:i:s', or null if never. */
        public readonly ?string $updated = null,
        /**
         * Set when a key IS stored but could not be decrypted — a restored backup, a rotated
         * `BLOWFISH_KEY`. Kept apart from "no key" because the fix differs: re-enter the key here,
         * versus enter one for the first time. {@see SecretBox} explains when this happens.
         */
        public readonly bool $apiKeyUnreadable = false,
    ) {
    }

    /** Nothing has been set up yet — no provider chosen. */
    public function isEmpty(): bool
    {
        return $this->provider === null;
    }

    /**
     * Whether an outbound call could actually be made: a provider, a usable key, a model, and — for
     * {@see AiProviders::COMPATIBLE}, which has no default — a base URL.
     */
    public function isComplete(): bool
    {
        return $this->provider !== null
            && $this->apiKeyConfigured
            && !$this->apiKeyUnreadable
            && $this->model !== ''
            && $this->effectiveBaseUrl() !== '';
    }

    /**
     * Whether the CONNECTION can be tested — i.e. whether `GET /models` can be called.
     *
     * Everything {@see self::isComplete()} wants EXCEPT the model, because the model listing is the
     * only place a merchant can learn the model ids from, and it is free and needs no model itself
     * ({@see AiConnectionTester}). Gating the test on a model made the first setup a chicken-and-egg:
     * you had to invent a model name to be allowed to ask which model names exist. A model is
     * required to RUN a call, never to ASK whether the key works.
     */
    public function isConnectionTestable(): bool
    {
        return $this->provider !== null
            && $this->apiKeyConfigured
            && !$this->apiKeyUnreadable
            && $this->effectiveBaseUrl() !== '';
    }

    /**
     * Why {@see self::isConnectionTestable()} is false — {@see self::missingReason()} without the
     * model sentence, which does not block a test.
     */
    public function connectionMissingReason(): string
    {
        if ($this->model === '') {
            // Ask the same ladder about a configuration that only lacks the model, so the wording
            // stays in one place instead of being duplicated here.
            return (new self(
                $this->provider,
                'probe',
                $this->baseUrl,
                $this->apiKeyConfigured,
                $this->apiKeyLast4,
                $this->updated,
                $this->apiKeyUnreadable,
            ))->missingReason();
        }

        return $this->missingReason();
    }

    /** The API root to call: the merchant's override if given, else the provider's default. */
    public function effectiveBaseUrl(): string
    {
        if ($this->baseUrl !== '') {
            return $this->baseUrl;
        }

        return $this->provider?->defaultBaseUrl() ?? '';
    }

    /**
     * Why {@see self::isComplete()} is false, in a sentence a merchant or an LLM can act on.
     * Empty string when nothing is missing.
     */
    public function missingReason(): string
    {
        if ($this->provider === null) {
            return 'No AI provider is configured on this shop.';
        }
        if ($this->apiKeyUnreadable) {
            return 'The stored API key cannot be decrypted — it was saved under a different shop '
                . 'installation secret (restored backup or rotated BLOWFISH_KEY). Save the key again.';
        }
        if (!$this->apiKeyConfigured) {
            return 'No API key is stored for provider ' . $this->provider->value . '.';
        }
        if ($this->model === '') {
            return 'No default model is configured.';
        }
        if ($this->effectiveBaseUrl() === '') {
            return 'Provider ' . $this->provider->value . ' has no default API URL, so a baseUrl '
                . 'must be configured.';
        }

        return '';
    }

    /**
     * @return array{
     *     provider: string|null,
     *     providerLabel: string|null,
     *     model: string,
     *     baseUrl: string,
     *     apiKeyConfigured: bool,
     *     apiKeyLast4: string,
     *     apiKeyUnreadable: bool,
     *     complete: bool,
     *     updated: string|null
     * }
     */
    public function jsonSerialize(): array
    {
        return [
            'provider'         => $this->provider?->value,
            'providerLabel'    => $this->provider?->label(),
            'model'            => $this->model,
            'baseUrl'          => $this->effectiveBaseUrl(),
            'apiKeyConfigured' => $this->apiKeyConfigured,
            'apiKeyLast4'      => $this->apiKeyLast4,
            'apiKeyUnreadable' => $this->apiKeyUnreadable,
            'complete'         => $this->isComplete(),
            'updated'          => $this->updated,
        ];
    }
}
