<?php
/**
 * Copyright since 2025 InPost S.A.
 *
 * For the full license information, please view the LICENSE file bundled with the module.
 *
 * @author InPost S.A.
 * @copyright since 2025 InPost S.A.
 * @license MIT
 */

declare(strict_types=1);

namespace InPost\International\OAuth2;

use InPost\International\Clock\SystemClock;
use InPost\International\OAuth2\Authentication\ClientCredentialsInterface;
use InPost\International\OAuth2\Grant\GrantTypeInterface;
use InPost\International\OAuth2\Grant\RefreshTokenGrant;
use InPost\International\OAuth2\Token\AccessTokenFactoryInterface;
use InPost\International\OAuth2\Token\AccessTokenInterface;
use InPost\International\OAuth2\Token\AccessTokenRepositoryInterface;
use InPost\International\OAuth2\Token\BearerTokenFactory;
use InPost\International\OAuth2\Token\InMemoryTokenRepository;
use Psr\Clock\ClockInterface;
use Symfony\Component\HttpFoundation\Request;

if (!defined('_PS_VERSION_')) {
    exit;
}

final class AuthorizationProvider implements AuthorizationProviderInterface
{
    /**
     * @var AuthorizationServerClientInterface
     */
    private $client;

    /**
     * @var GrantTypeInterface
     */
    private $grantType;

    /**
     * @var ClientCredentialsInterface
     */
    private $credentials;

    /**
     * @var AccessTokenRepositoryInterface
     */
    private $tokenRepository;

    /**
     * @var AccessTokenFactoryInterface
     */
    private $tokenFactory;

    /**
     * @var ClockInterface
     */
    private $clock;

    public function __construct(AuthorizationServerClientInterface $client, GrantTypeInterface $grantType, ClientCredentialsInterface $credentials, AccessTokenRepositoryInterface $tokenRepository = null, AccessTokenFactoryInterface $tokenFactory = null, ClockInterface $clock = null)
    {
        $this->client = $client;
        $this->grantType = $grantType;
        $this->credentials = $credentials;
        $this->tokenRepository = $tokenRepository ?? new InMemoryTokenRepository();
        $this->tokenFactory = $tokenFactory ?? new BearerTokenFactory();
        $this->clock = $clock ?? SystemClock::fromSystemTimezone();
    }

    /**
     * {@inheritDoc}
     */
    public function authorize(array $scopes = []): void
    {
        $this->grantType->authorize($this->client, $this->credentials, $scopes);
    }

    /**
     * {@inheritDoc}
     */
    public function processAuthorizationResponse(Request $request): void
    {
        $this->grantType->processAuthorizationResponse($request);
    }

    /**
     * {@inheritDoc}
     */
    public function getAccessToken(bool $renew = false, array $scopes = []): AccessTokenInterface
    {
        if (!$renew && $token = $this->getOrRefreshStoredToken($scopes)) {
            return $token;
        }

        $data = $this->grantType->getAccessToken($this->client, $this->credentials, $scopes);

        return $this->createAndSaveToken($data);
    }

    private function getOrRefreshStoredToken(array $scopes): ?AccessTokenInterface
    {
        if (!$token = $this->tokenRepository->getAccessToken()) {
            return null;
        }

        if (!$this->hasRequestedScopes($token, $scopes)) {
            return null;
        }

        if (!$this->isExpired($token)) {
            return $token;
        }

        if (!$refreshToken = $token->getRefreshToken()) {
            return null;
        }

        return $this->refreshAccessToken($refreshToken);
    }

    private function hasRequestedScopes(AccessTokenInterface $token, array $scopes): bool
    {
        if ([] === $scopes) {
            return true;
        }

        if (!$tokenScopes = $token->getScopes()) {
            return false;
        }

        return [] === array_diff($scopes, $tokenScopes);
    }

    private function isExpired(AccessTokenInterface $token): bool
    {
        $expiresAt = $token->getExpiresAt();

        return null !== $expiresAt && $expiresAt <= $this->clock->now();
    }

    private function refreshAccessToken(string $refreshToken): AccessTokenInterface
    {
        $grant = new RefreshTokenGrant($refreshToken);
        $data = $grant->getAccessToken($this->client, $this->credentials);

        return $this->createAndSaveToken($data);
    }

    private function createAndSaveToken(array $data): AccessTokenInterface
    {
        $token = $this->tokenFactory->createToken($data);
        $this->tokenRepository->saveAccessToken($token);

        return $token;
    }
}
