<?php

declare(strict_types=1);

final class MyronRoleScopeProjector
{
    /**
     * @return list<array{input: string, normalized: string, runtimeRoles: list<string>, lossy: bool, notes: string}>
     */
    public static function catalog(): array
    {
        return [
            self::describeRole('Designer'),
            self::describeRole('ContentManager'),
            self::describeRole('Editor'),
            self::describeRole('Admin'),
        ];
    }

    /**
     * @return list<string>
     */
    public static function projectRole(string $role): array
    {
        return self::describeRole($role)['runtimeRoles'];
    }

    /**
     * @param array<string, mixed>|null $roleScope
     * @return array<string, list<string>>|null
     */
    public static function projectRoleScope(?array $roleScope): ?array
    {
        if ($roleScope === null) {
            return null;
        }

        $projected = [];

        foreach ($roleScope as $operation => $roles) {
            if (!is_array($roles)) {
                throw new InvalidArgumentException('Role scope operation `' . (string) $operation . '` must be a list of role names.');
            }

            $runtimeRoles = [];
            foreach ($roles as $role) {
                if (!is_string($role) || trim($role) === '') {
                    throw new InvalidArgumentException('Role scope entries must be non-empty strings.');
                }

                foreach (self::projectRole($role) as $runtimeRole) {
                    $runtimeRoles[$runtimeRole] = true;
                }
            }

            $projected[(string) $operation] = array_keys($runtimeRoles);
        }

        return $projected;
    }

    /**
     * @param list<string> $roles
     */
    public static function userMatchesRoles(array $user, array $roles): bool
    {
        $effectiveRole = trim((string) ($user['effective_role_key'] ?? $user['role_key'] ?? ''));
        if ($effectiveRole === '') {
            return false;
        }

        $allowedRoles = [];
        foreach ($roles as $role) {
            foreach (self::projectRole($role) as $runtimeRole) {
                $allowedRoles[$runtimeRole] = true;
            }
        }

        return isset($allowedRoles[$effectiveRole]);
    }

    /**
     * @return array{input: string, normalized: string, runtimeRoles: list<string>, lossy: bool, notes: string}
     */
    public static function describeRole(string $role): array
    {
        $trimmed = trim($role);
        if ($trimmed === '') {
            throw new InvalidArgumentException('Role name is required for projection.');
        }

        if (in_array($trimmed, ['viewer', 'designer', 'editor', 'administrator'], true)) {
            return [
                'input' => $trimmed,
                'normalized' => $trimmed,
                'runtimeRoles' => [$trimmed],
                'lossy' => false,
                'notes' => 'Runtime role key passthrough.',
            ];
        }

        $normalized = preg_replace('/[^a-z]+/', '', strtolower($trimmed)) ?? '';

        return match ($normalized) {
            'designer' => [
                'input' => $trimmed,
                'normalized' => $normalized,
                'runtimeRoles' => ['designer', 'administrator'],
                'lossy' => false,
                'notes' => 'Designer authority maps directly to the current designer role; administrators retain superset access.',
            ],
            'contentmanager' => [
                'input' => $trimmed,
                'normalized' => $normalized,
                'runtimeRoles' => ['editor', 'administrator'],
                'lossy' => true,
                'notes' => 'The current runtime does not distinguish Content Manager from Editor, so both project to editor-level content authority plus administrator.',
            ],
            'editor' => [
                'input' => $trimmed,
                'normalized' => $normalized,
                'runtimeRoles' => ['editor', 'administrator'],
                'lossy' => true,
                'notes' => 'The current runtime collapses editorial approval into the editor role until a dedicated editorial permission surface exists.',
            ],
            'admin', 'administrator' => [
                'input' => $trimmed,
                'normalized' => $normalized,
                'runtimeRoles' => ['administrator'],
                'lossy' => false,
                'notes' => 'Admin authority maps directly to the administrator runtime role.',
            ],
            'viewer' => [
                'input' => $trimmed,
                'normalized' => $normalized,
                'runtimeRoles' => ['viewer', 'administrator'],
                'lossy' => false,
                'notes' => 'Viewer access remains read-only in the current runtime; administrators retain superset access.',
            ],
            default => throw new InvalidArgumentException('Unknown definitions/runtime role `' . $trimmed . '` cannot be projected.'),
        };
    }
}
