<?php

declare(strict_types=1);

namespace Plugin\tnt_mcp\src\Mcp\Auth;

use JTL\DB\DbInterface;
use RuntimeException;

/**
 * Per-shop bearer-token auth + scope resolution. Tokens are issued/revoked in the plugin admin
 * page and stored ONLY as a sha256 hash (never in clear). Deny-by-default: an unknown or revoked
 * token is rejected BEFORE any tool runs; a known token grants exactly its recorded scopes.
 */
final class TokenGuard
{
    /**
     * @param list<string> $enabledScopes scopes granted to the presented token
     * @param string       $tokenId       stable id/label for the token, used in the audit log
     */
    public function __construct(
        private readonly array $enabledScopes,
        private readonly string $tokenId = '',
    ) {
    }

    /**
     * Resolve a presented bearer token against tnt_mcp_token. Throws on an unknown or revoked
     * token (deny-by-default); returns a guard carrying the token's scopes on success. Records
     * dLastUsed for observability. A valid token with zero scopes still authenticates — the
     * per-tool scope check then denies it, which is audited distinctly from a bad token.
     *
     * @param non-empty-string $bearer
     */
    public static function fromRequest(string $bearer, DbInterface $db): self
    {
        $row = $db->getSingleObject(
            'SELECT kToken, cScopes, nActive FROM tnt_mcp_token WHERE cTokenHash = :h',
            ['h' => hash('sha256', $bearer)],
        );

        if ($row === null || (int)$row->nActive !== 1) {
            throw new RuntimeException('invalid or revoked token');
        }

        $decoded = json_decode((string)$row->cScopes, true);
        $scopes  = [];
        if (\is_array($decoded)) {
            foreach ($decoded as $scope) {
                if (\is_string($scope) && $scope !== '') {
                    $scopes[] = $scope;
                }
            }
        }

        $db->queryPrepared(
            'UPDATE tnt_mcp_token SET dLastUsed = NOW() WHERE kToken = :id',
            ['id' => (int)$row->kToken],
        );

        return new self($scopes, 'tok_' . (int)$row->kToken);
    }

    public function allows(string $scope): bool
    {
        return \in_array($scope, $this->enabledScopes, true);
    }

    public function tokenId(): string
    {
        return $this->tokenId;
    }

    /**
     * What this token DOES carry — for the refusal, so a client can tell a merchant which scope to
     * ask an admin for instead of guessing. No secret is in a scope name; the token itself is never
     * held here in clear.
     *
     * @return list<string>
     */
    public function scopes(): array
    {
        return $this->enabledScopes;
    }
}
