<?php

declare(strict_types=1);

namespace NoahVet\Reef\Security\Voter;

use NoahVet\Reef\Factory\ClientFactory;
use NoahVet\Reef\Jane\Client;
use NoahVet\Reef\Jane\Model\PermissionGrant;
use NoahVet\Reef\Jane\Model\PermissionGrantList;
use NoahVet\Reef\Security\Authentication\BearerClientHMacComputer;
use NoahVet\Reef\Security\Authentication\Token\ReefOAuthToken;
use NoahVet\Reef\Security\IAM\Mapper\IAMResourceMapper;
use NoahVet\Reef\Security\IAM\Model\Resource;
use NoahVet\Reef\Security\IAM\Model\ResourceType;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;

class IAMVoter implements IAMVoterInterface
{
    protected BearerClientHMacComputer $clientHMacComputer;

    protected IAMResourceMapper $mapper;

    protected ClientFactory $clientFactory;

    /**
     * @var array<string, Client>
     */
    protected array $clients;

    public function __construct(
        BearerClientHMacComputer $clientHMacComputer,
        IAMResourceMapper $mapper,
        ClientFactory $clientFactory,
    ) {
        $this->clientHMacComputer = $clientHMacComputer;
        $this->mapper = $mapper;
        $this->clientFactory = $clientFactory;
        $this->clients = [];
    }

    /**
     * @psalm-suppress ArgumentTypeCoercion
     */
    public function vote(TokenInterface $token, mixed $subject, array $attributes): int
    {
        $bearer = null;

        if ($token instanceof ReefOAuthToken) {
            $bearer = $token->getBearerToken();
        }
        /* @psalm-suppress RedundantCondition */
        if (\is_a($token, 'HWI\Bundle\OAuthBundle\Security\Core\Authentication\Token\OAuthToken')) {
            $bearer = $token->getAccessToken();
        }

        if (null === $bearer) {
            return self::ACCESS_ABSTAIN;
        }

        if (!isset($this->clients[$bearer])) {
            $this->clients[$bearer] = $this->clientFactory->create($bearer);
        }

        // When asked for super admin only, check if the user is super-admin
        if (1 === \count($attributes) && \str_starts_with($attributes[0], 'reef:iam:principals:')) {
            return $this->voteUserGroup($this->clients[$bearer], $attributes[0]);
        }

        if ($subject instanceof ResourceType) {
            return $this->voteResourceType(
                $this->clients[$bearer],
                $bearer,
                (string) $subject,
                $attributes,
            );
        }

        if ($subject instanceof Resource) {
            $subjectSlug = (string) $subject;
        } else {
            $subjectSlug = $this->mapper->map($subject)?->__toString();
        }

        return $this->voteResource(
            $this->clients[$bearer],
            $bearer,
            $subjectSlug,
            $attributes,
        );
    }

    /**
     * @return int either ACCESS_GRANTED, ACCESS_ABSTAIN, or ACCESS_DENIED
     *
     * @psalm-return self::ACCESS_* must be transformed into @return on Symfony 7
     */
    protected function voteUserGroup(Client $client, string $groupSlug): int
    {
        /** @var \NoahVet\Reef\Jane\Model\UserinfoGetResponse200|null $userinfo */
        $userinfo = $client->userinfo();
        if (
            \in_array($groupSlug, $userinfo?->getGroups() ?? [])
        ) {
            return self::ACCESS_GRANTED;
        } else {
            return self::ACCESS_DENIED;
        }
    }

    /**
     * @param array<mixed> $attributes
     *
     * @return int either ACCESS_GRANTED, ACCESS_ABSTAIN, or ACCESS_DENIED
     *
     * @psalm-return self::ACCESS_* must be transformed into @return on Symfony 7
     */
    protected function voteResource(
        Client $client,
        string $bearer,
        ?string $subjectSlug,
        array $attributes,
    ): int {
        if (null === $subjectSlug) {
            return self::ACCESS_ABSTAIN;
        }

        $permissions = $client->getDetailedPermissionsResourceItem(
            $subjectSlug,
            $this->clientHMacComputer->computeClientHMacHeader($bearer),
        );

        \assert($permissions instanceof PermissionGrant);

        $allowedPermissions = \array_intersect($attributes, $permissions->getAllowed());

        return \count($allowedPermissions) === \count($attributes) ? self::ACCESS_GRANTED : self::ACCESS_DENIED;
    }

    /**
     * @param array<mixed> $attributes
     *
     * @return int either ACCESS_GRANTED, ACCESS_ABSTAIN, or ACCESS_DENIED
     *
     * @psalm-return self::ACCESS_* must be transformed into @return on Symfony 7
     */
    protected function voteResourceType(
        Client $client,
        string $bearer,
        ?string $subjectSlug,
        array $attributes,
    ): int {
        if (null === $subjectSlug) {
            return self::ACCESS_ABSTAIN;
        }

        $permissions = $client->getDetailedPermissionsResourceTypeItem(
            $subjectSlug,
            [],
            $this->clientHMacComputer->computeClientHMacHeader($bearer),
        );

        \assert($permissions instanceof PermissionGrantList);

        /** @var array<string, PermissionGrant>|\ArrayObject<string, PermissionGrant> $resourcesGrants */
        $resourcesGrants = $permissions->getResources();

        if (!\is_array($resourcesGrants)) {
            $resourcesGrants = $resourcesGrants->getArrayCopy();
        }

        $resourceAllowed = \array_reduce(
            \array_values($resourcesGrants),
            static fn (bool $carry, PermissionGrant $grant): bool => $carry || \count(\array_intersect($grant->getAllowed(), $attributes)) === \count($attributes),
            false,
        );

        /** @var array<string, PermissionGrant>|\ArrayObject<string, PermissionGrant> $groupsGrants */
        $groupsGrants = $permissions->getGroups();

        if (!\is_array($groupsGrants)) {
            $groupsGrants = $groupsGrants->getArrayCopy();
        }

        $groupAllowed = \array_reduce(
            \array_values($groupsGrants),
            static fn (bool $carry, PermissionGrant $grant): bool => $carry || \count(\array_intersect($grant->getAllowed(), $attributes)) === \count($attributes),
            false,
        );

        return $resourceAllowed || $groupAllowed ? self::ACCESS_GRANTED : self::ACCESS_DENIED;
    }
}
