<?php

namespace App\Security\Voter\EntityVoter;

use App\Entity\Access\Access;
use App\Service\Access\Utils;
use App\Service\Security\InternalRequestsService;
use App\Service\Security\SwitchUser;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\SecurityBundle\Security;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\Voter\Voter;

/**
 * Base class for custom Voters
 *
 * This class also defines a default behavior for entity based voters (ex: FileVoter)
 *
 * @see doc/security.md
 */
abstract class AbstractEntityVoter extends Voter
{
    protected const READ = 'READ';
    protected const EDIT = 'EDIT';
    protected const CREATE = 'CREATE';
    protected const DELETE = 'DELETE';

    /**
     * The current user if any; access it trough isUserLoggedIn or getUser methods
     * If the current user is null, it has not been fetched already
     * If it is false, there is no user logged in
     * @var Access|null|false
     */
    private Access|null|false $user = null;

    /**
     * The supported class name. Override it in subclass.
     * Ex:
     *     protected ?string $entityClass = File::class;
     *
     * @var string|null
     */
    protected static ?string $entityClass = null;

    /**
     * List of supported operations. Override it to restrict.
     * @var array<string>
     */
    protected static array $allowedOperations = [
        self::READ, self::EDIT, self::CREATE, self::DELETE
    ];

    public function __construct(
        protected Security $security,
        protected Utils $accessUtils,
        private InternalRequestsService $internalRequestsService,
        private EntityManagerInterface $em,
        private SwitchUser $switchUser
    ) {}

    /**
     * Default `supports` method, that uses self::entityClass and self::allowedOperations to determine if the voter
     * supports the subject and attribute.
     *
     * @param string $attribute
     * @param mixed $subject
     * @return bool
     */
    protected function supports(string $attribute, mixed $subject): bool
    {
        if (static::$entityClass === null) {
            throw new \RuntimeException('Setup the self::$entityClass property, or override the supports() method');
        }

        return $subject !== null && $subject::class === static::$entityClass && in_array($attribute, static::$allowedOperations);
    }

    /**
     * Default `voteOnAttribute` method, calling one of the `canXxxx()` method, according to the attribute.
     *
     * @param string $attribute
     * @param mixed $subject
     * @param TokenInterface $token
     * @return bool
     */
    protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token): bool
    {
        switch ($attribute) {
            case self::READ:
                return $this->canView($subject);
            case self::EDIT:
                return $this->canEdit($subject);
            case self::CREATE:
                return $this->canCreate($subject);
            case self::DELETE:
                return $this->canDelete($subject);
        }

        return false;
    }

    /**
     * Does the client have the right to view this resource?
     * @param object $subject
     * @return bool
     */
    protected function canView(object $subject): bool {
        return false;
    }

    /**
     * Does the client have the right to edit this resource?
     * @param object $subject
     * @return bool
     */
    protected function canEdit(object $subject): bool {
        return false;
    }

    /**
     * Does the client have the right to create this resource?
     *
     * @param object $subject
     * @return bool
     */
    protected function canCreate(object $subject): bool {
        return false;
    }

    /**
     * Does the client have the right to delete this resource?
     *
     * @param object $subject
     * @return bool
     */
    protected function canDelete(object $subject): bool {
        return false;
    }

    /**
     * Returns the current logged in user
     * @return Access
     */
    protected function getUser(): ?Access {
        if ($this->user === null) {
            /** @var Access $user */
            $user = $this->security->getUser();

            // <-- Special case of impersonated users: the switch user is not setup yet by symfony, we have to do it "manually"
            $switchHeaderId = $_SERVER['HTTP_X_SWITCH_USER'] ?? null;
            if ($switchHeaderId !== null) {
                $switchAs = $this->em->find(Access::class, $switchHeaderId);
                if (
                    $switchAs &&
                    (
                        $this->security->isGranted('ROLE_ALLOWED_TO_SWITCH') ||
                        $this->switchUser->isAllowedToSwitch($user, $switchAs)
                    )
                ) {
                    $user = $switchAs;
                }
            }
            // -->

            // If the user is not anonymous, remember it
            $this->user = $user instanceof Access ? $user : false;
        }

        return $this->user !== false ? $this->user : null;
    }

    /**
     * Is the client an authenticated user ?
     *
     * @return bool
     */
    protected function isUserLoggedIn(): bool {
        return $this->getUser() !== null;
    }

    /**
     * Is the current request a valid internal request?
     *
     * @see doc/internal_requests.md
     * @see doc/security.md
     *
     * @return bool
     */
    protected function isValidInternalRequest(): bool {
        $clientIp = $_SERVER['REMOTE_ADDR'] ?? null;
        $internalRequestToken = $_SERVER['HTTP_INTERNAL_REQUESTS_TOKEN'] ?? '';

        return $this->internalRequestsService->isAllowed($clientIp, $internalRequestToken);
    }
}
