<?php

declare(strict_types=1);

namespace Plugin\tnt_mcp\src\Mcp\OAuth\Repositories;

use JTL\DB\DbInterface;
use League\OAuth2\Server\Entities\ClientEntityInterface;
use League\OAuth2\Server\Repositories\ClientRepositoryInterface;
use Plugin\tnt_mcp\src\Mcp\OAuth\Entities\ClientEntity;
use stdClass;

/**
 * DB-backed client store (table tnt_mcp_oauth_client). Clients are created by the dynamic
 * registration endpoint; nearly all are PUBLIC (PKCE, no secret) because that is what the
 * ChatGPT/Claude connectors register. A confidential client (secret issued) is still supported and
 * its secret is verified against a sha256 hash in constant time — the clear secret is never stored.
 */
final class ClientRepository implements ClientRepositoryInterface
{
    public function __construct(private readonly DbInterface $db)
    {
    }

    public function getClientEntity(string $clientIdentifier): ?ClientEntityInterface
    {
        $row = $this->row($clientIdentifier);
        if ($row === null) {
            return null;
        }

        return $this->hydrate($row);
    }

    public function validateClient(string $clientIdentifier, ?string $clientSecret, ?string $grantType): bool
    {
        $row = $this->row($clientIdentifier);
        if ($row === null) {
            return false;
        }

        // Public client (registered with token_endpoint_auth_method=none): no secret, PKCE proves
        // possession. league only reaches confidential verification when a secret is presented.
        if ((int)$row->nConfidential !== 1) {
            return true;
        }

        if ($clientSecret === null || $clientSecret === '') {
            return false;
        }

        return hash_equals((string)$row->cSecretHash, hash('sha256', $clientSecret));
    }

    private function row(string $clientIdentifier): ?stdClass
    {
        return $this->db->getSingleObject(
            'SELECT cClientId, cName, cRedirectUris, nConfidential, cSecretHash
                 FROM tnt_mcp_oauth_client WHERE cClientId = :id',
            ['id' => $clientIdentifier],
        );
    }

    /** Null for a corrupt row whose client_id is empty — such a client can never be matched anyway. */
    private function hydrate(stdClass $row): ?ClientEntity
    {
        $clientId = (string)$row->cClientId;
        if ($clientId === '') {
            return null;
        }

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

        return new ClientEntity(
            $clientId,
            (string)$row->cName,
            $redirects,
            (int)$row->nConfidential === 1,
        );
    }
}
