<?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\Grant;

use InPost\International\OAuth2\Authentication\ClientCredentialsInterface;
use InPost\International\OAuth2\AuthorizationServerClientInterface;
use InPost\International\OAuth2\Exception\AuthorizationRequestException;
use InPost\International\OAuth2\Exception\ReauthorizationRequiredException;
use InPost\International\OAuth2\Exception\RuntimeException;
use InPost\International\OAuth2\Exception\UnexpectedValueException;
use InPost\International\OAuth2\PKCE\PkceMethodInterface;
use InPost\International\Storage\StorageInterface;
use Symfony\Component\HttpFoundation\Request;

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

final class AuthorizationCodeGrant extends AbstractGrant
{
    public const IDENTIFIER = 'authorization_code';

    private const STATE_STORAGE_KEY = 'oauth2_state';
    private const CODE_VERIFIER_STORAGE_KEY = 'oauth2_code_verifier';

    /**
     * @var StorageInterface
     */
    private $storage;

    /**
     * @var string|null
     */
    private $redirectUri;

    /**
     * @var PkceMethodInterface|null
     */
    private $pkceMethod;

    /**
     * @var string|null
     */
    private $code;

    public function __construct(StorageInterface $storage, string $redirectUri = null, PkceMethodInterface $pkceMethod = null)
    {
        $this->storage = $storage;
        $this->redirectUri = $redirectUri;
        $this->pkceMethod = $pkceMethod;
    }

    public function getIdentifier(): string
    {
        return self::IDENTIFIER;
    }

    public function authorize(AuthorizationServerClientInterface $authServerClient, ClientCredentialsInterface $credentials, array $scopes = [])
    {
        $params = $this->getAuthorizationRequestParameters($credentials, $scopes);

        $authServerClient->redirectToAuthorizationEndpoint($params);
    }

    public function processAuthorizationResponse(Request $request): void
    {
        $queryParams = $request->query->all();

        if (!isset($queryParams['state'])) {
            throw new UnexpectedValueException('State parameter is missing in the authorization response.');
        }

        if (null === $state = $this->storage->remove(self::STATE_STORAGE_KEY)) {
            throw new RuntimeException('State parameter was not found in the storage.');
        }

        if ($queryParams['state'] !== $state) {
            throw new UnexpectedValueException('State mismatch.');
        }

        if (isset($queryParams['error'])) {
            throw AuthorizationRequestException::create($queryParams);
        }

        if (!isset($queryParams['code'])) {
            throw new UnexpectedValueException('Authorization code is missing in the authorization response.');
        }

        $this->code = $queryParams['code'];
    }

    protected function getAccessTokenRequestParameters(ClientCredentialsInterface $credentials, array $scopes): array
    {
        if (!isset($this->code)) {
            throw ReauthorizationRequiredException::create();
        }

        $params = parent::getAccessTokenRequestParameters($credentials, $scopes);
        $params['code'] = $this->code;

        if (null !== $this->redirectUri) {
            $params['redirect_uri'] = $this->redirectUri;
        }

        if (null === $this->pkceMethod) {
            return $params;
        }

        if (null === $codeVerifier = $this->storage->remove(self::CODE_VERIFIER_STORAGE_KEY)) {
            throw new RuntimeException('Code verifier was not found in the storage.');
        }

        $params['code_verifier'] = $codeVerifier;

        return $params;
    }

    private function getAuthorizationRequestParameters(ClientCredentialsInterface $clientCredentials, array $scopes): array
    {
        $params = [
            'response_type' => 'code',
            'client_id' => $clientCredentials->getClientId(),
            'state' => $this->generateState(),
        ];

        if (null !== $this->redirectUri) {
            $params['redirect_uri'] = $this->redirectUri;
        }

        if (null !== $this->pkceMethod) {
            $params['code_challenge'] = $this->pkceMethod->getCodeChallenge();
            $params['code_challenge_method'] = $this->pkceMethod->getIdentifier();

            $this->storage->set(self::CODE_VERIFIER_STORAGE_KEY, $this->pkceMethod->getCodeVerifier());
        }

        if ([] !== $scopes) {
            $params['scope'] = implode(' ', $scopes);
        }

        return $params;
    }

    private function generateState(): string
    {
        $state = bin2hex(random_bytes(16));
        $this->storage->set(self::STATE_STORAGE_KEY, $state);

        return $state;
    }
}
