<?php

declare(strict_types=1);

namespace Plugin\tnt_mcp\src\Mcp\Admin;

use JTL\DB\DbInterface;
use Plugin\tnt_mcp\src\Mcp\OAuth\Entities\ShopUserEntity;
use Throwable;

/**
 * Read model behind the Q2 warning row on the "Integrationen" tab: which MCP connections are LIVE
 * right now, and — per connection — which backend administrator approved it and therefore whose
 * rights the connected client is wielding.
 *
 * The approving admin is recorded on the access token as `cUserId`
 * ({@see \Plugin\tnt_mcp\src\Mcp\Backend\BackendIdentityFactory::forTokenUser()}). A token issued
 * before admin-identity binding carries the literal {@see ShopUserEntity::IDENTIFIER} and is shown
 * as such — it cannot act at all and must be re-authorised.
 *
 * Read-only and best-effort, like its sibling {@see TokenAuditOverview}: a missing table degrades to
 * an empty list rather than an exception, and no secret is ever surfaced.
 */
final class ConsentReport
{
    public function __construct(private readonly DbInterface $db)
    {
    }

    /**
     * One row per live (non-revoked, unexpired) access token.
     *
     * @return list<array{
     *     client: string, clientId: string, admin: string, adminKnown: bool, legacy: bool,
     *     superAdmin: bool, scopes: list<string>, expires: string
     * }>
     */
    public function liveConsents(int $limit = 50): array
    {
        $rows = $this->query(
            'SELECT t.cUserId AS userId, t.cScopes AS scopes, t.dExpires AS expires,
                    t.cClientId AS clientId, c.cName AS clientName,
                    a.cLogin AS adminLogin, a.kAdminlogingruppe AS groupId
               FROM tnt_mcp_oauth_access_token t
               LEFT JOIN tnt_mcp_oauth_client c ON c.cClientId = t.cClientId
               LEFT JOIN tadminlogin a ON a.cLogin = t.cUserId
              WHERE t.nRevoked = 0 AND t.dExpires > NOW()
              ORDER BY t.dCreated DESC
              LIMIT ' . \max(1, \min($limit, 200)),
        );

        $out = [];
        foreach ($rows as $row) {
            $userID = (string)($row->userId ?? '');
            $legacy = $userID === '' || $userID === ShopUserEntity::IDENTIFIER;
            $login  = (string)($row->adminLogin ?? '');
            $client = (string)($row->clientName ?? '');

            $out[] = [
                'client'     => $client !== '' ? $client : (string)($row->clientId ?? ''),
                'clientId'   => (string)($row->clientId ?? ''),
                'admin'      => $legacy ? '' : ($login !== '' ? $login : $userID),
                'adminKnown' => !$legacy && $login !== '',
                'legacy'     => $legacy,
                'superAdmin' => !$legacy && (int)($row->groupId ?? 0) === \ADMINGROUP,
                'scopes'     => $this->decodeScopes($row->scopes ?? null),
                'expires'    => (string)($row->expires ?? ''),
            ];
        }

        return $out;
    }

    /**
     * The standing Q2 warning, shown whether or not a token exists — the point is that connecting
     * one grants full rights, which the merchant has to know BEFORE connecting.
     *
     * @return array{title: array{de: string, en: string}, text: array{de: string, en: string}}
     */
    public static function warning(): array
    {
        return [
            'title' => [
                'de' => 'Ein verbundener MCP-Client handelt mit Ihren vollen Backend-Rechten',
                'en' => 'A connected MCP client acts with your full backend permissions',
            ],
            'text' => [
                'de' => 'Wer eine Verbindung freigibt, gibt sie mit seinen eigenen Rechten frei: '
                    . 'Der MCP-Client handelt anschließend im Backend als genau dieser Administrator '
                    . 'und kann alles tun, was dieser Administrator tun darf — auch schreibend. '
                    . 'Es gibt keine zusätzliche Rechteprüfung pro Anfrage. Geben Sie Verbindungen '
                    . 'deshalb nur für Clients frei, denen Sie vertrauen, und widerrufen Sie nicht '
                    . 'mehr benötigte Tokens unter „Tokens & Audit“. Im Betriebsmodus '
                    . '„Eingeschränkt“ sind ausschließlich umkehrbare Aktionen erlaubt.',
                'en' => 'Whoever approves a connection approves it with their own permissions: the '
                    . 'MCP client then acts in the backend as exactly that administrator and can do '
                    . 'anything that administrator may do, including writes. There is no additional '
                    . 'per-request permission check. Only approve connections for clients you '
                    . 'trust, and revoke tokens you no longer need under "Tokens & Audit". In '
                    . '"limited" operating mode only reversible actions are allowed.',
            ],
        ];
    }

    /**
     * @return list<\stdClass>
     */
    private function query(string $sql): array
    {
        try {
            // getObjects() is typed array<stdClass>; a fetch-all result is always sequential, and
            // array_values() makes that guarantee explicit for the list<> contract above.
            return \array_values($this->db->getObjects($sql));
        } catch (Throwable) {
            return [];
        }
    }

    /** @return list<string> */
    private function decodeScopes(mixed $raw): array
    {
        if (!\is_string($raw) || $raw === '') {
            return [];
        }
        $decoded = \json_decode($raw, true);
        if (!\is_array($decoded)) {
            return [];
        }

        $out = [];
        foreach ($decoded as $scope) {
            if (\is_string($scope) && $scope !== '') {
                $out[] = $scope;
            }
        }

        return $out;
    }
}
