<?php

declare(strict_types=1);

namespace Plugin\tnt_mcp\src\Ai;

/**
 * What the LLM connection last did, as stored — pure observation, no judgement.
 *
 * The judgement ("Aktiv" / "Eingeschränkt" / "Gestört") is {@see AiStatus}, and it is deliberately
 * NOT persisted: it depends on the configuration, the budget and on *how long ago* the signals
 * were, so a stored verdict would start lying the moment the clock moved. This object stores only
 * facts with timestamps, and the verdict is derived at read time.
 */
final class AiStatusRecord implements \JsonSerializable
{
    /** The error kinds, in the order {@see self::classify()} tries them. Also the .po key suffixes. */
    public const KIND_AUTH = 'auth';

    public const KIND_NETWORK = 'network';

    public const KIND_MODEL = 'model';

    public const KIND_OTHER = 'other';

    /** Where a signal came from: an explicit test, or a real AI call the shop made anyway. */
    public const SOURCE_TEST = 'test';

    public const SOURCE_LIVE = 'live';

    public function __construct(
        /** 'Y-m-d H:i:s' of the last success, or null if there has never been one. */
        public readonly ?string $lastOkAt = null,
        public readonly ?string $lastErrorAt = null,
        /** One of the KIND_* constants, or '' when no error was ever recorded. */
        public readonly string $lastErrorKind = '',
        /** The provider's own message, for the "Gestört" detail line. Never contains the key. */
        public readonly string $lastErrorMessage = '',
        public readonly int $lastLatencyMs = 0,
        /** SOURCE_TEST or SOURCE_LIVE — '' when nothing has been recorded. */
        public readonly string $lastSource = '',
        /** The most recent signal of ANY kind; drives the TTL re-check. */
        public readonly ?string $lastCheckedAt = null,
    ) {
    }

    /** True when nothing has ever been observed — a fresh install, or a just-cleared config. */
    public function isBlank(): bool
    {
        return $this->lastOkAt === null && $this->lastErrorAt === null;
    }

    /**
     * Whether the most recent signal was a failure. A record with an error and no success at all
     * counts as failing; one with neither is not failing (it is simply unknown).
     */
    public function lastSignalFailed(): bool
    {
        if ($this->lastErrorAt === null) {
            return false;
        }

        return $this->lastOkAt === null || $this->lastErrorAt > $this->lastOkAt;
    }

    /**
     * Maps a provider response to one of the KIND_* constants.
     *
     * The point of the split is that each kind has a different fix, and the merchant is shown that
     * fix rather than the raw message: a rejected key is fixed at the provider, an unreachable host
     * is fixed in the network or the API address, an unknown model is fixed in the model field.
     *
     * A missing HTTP status means the request never got an answer — DNS, TLS, timeout — which is
     * exactly the network case; a status we cannot place is `other` rather than a guess.
     */
    public static function classify(?int $status): string
    {
        return match (true) {
            $status === null => self::KIND_NETWORK,
            $status === 401, $status === 403 => self::KIND_AUTH,
            $status === 404 => self::KIND_MODEL,
            $status >= 500 => self::KIND_NETWORK,
            default => self::KIND_OTHER,
        };
    }

    /**
     * @return array{
     *     lastOkAt: string|null,
     *     lastErrorAt: string|null,
     *     lastErrorKind: string,
     *     lastErrorMessage: string,
     *     lastLatencyMs: int,
     *     lastSource: string,
     *     lastCheckedAt: string|null
     * }
     */
    public function jsonSerialize(): array
    {
        return [
            'lastOkAt'         => $this->lastOkAt,
            'lastErrorAt'      => $this->lastErrorAt,
            'lastErrorKind'    => $this->lastErrorKind,
            'lastErrorMessage' => $this->lastErrorMessage,
            'lastLatencyMs'    => $this->lastLatencyMs,
            'lastSource'       => $this->lastSource,
            'lastCheckedAt'    => $this->lastCheckedAt,
        ];
    }
}
