<?php

declare(strict_types=1);

namespace App\Infrastructure\Auth;

use App\Domain\Auth\Role;
use Doctrine\DBAL\Connection;

/**
 * Resolves a user's role from their current OIDC group memberships using
 * the oidc_role_mappings table. Picks the *highest* role across all
 * matching groups (Admin > Operator > Viewer); falls back to the default
 * role if no group matches.
 */
final class RoleMappingRepository
{
    public function __construct(private readonly Connection $connection)
    {
    }

    /**
     * @param list<string> $groupIds
     */
    public function resolveRole(array $groupIds, Role $default): Role
    {
        // SEC_REVIEW F51: enforce the declared `list<string>` shape at
        // runtime before building the IN-clause. The static type is
        // PHPDoc-only; nothing stops a future caller from passing a
        // mixed array (or a hash with skipped indexes) and breaking
        // the placeholder count → bind-list invariant. `array_values`
        // re-keys to a clean 0..n-1 list; `array_filter` drops any
        // non-string entry. Matches the declared contract explicitly
        // so the SQL row count and the bind list always agree.
        $groupIds = array_values(array_filter($groupIds, 'is_string'));
        if ($groupIds === []) {
            return $default;
        }

        $placeholders = implode(', ', array_fill(0, count($groupIds), '?'));
        /** @var list<array<string, mixed>> $rows */
        $rows = $this->connection->fetchAllAssociative(
            sprintf('SELECT role FROM oidc_role_mappings WHERE group_id IN (%s)', $placeholders),
            $groupIds
        );

        if ($rows === []) {
            return $default;
        }

        $highest = null;
        foreach ($rows as $row) {
            $role = Role::tryFrom((string) $row['role']);
            if ($role === null) {
                continue;
            }
            if ($highest === null || $role->satisfies($highest)) {
                $highest = $role;
            }
        }

        return $highest ?? $default;
    }
}
