<?php

namespace Alsendo\AlsendoWrapper\Api\Ecolet;

use Alsendo\AlsendoWrapper\Api\Ecolet\Model\Token;
use Alsendo\AlsendoWrapper\Exception\ResponseException;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\GuzzleException;
use JsonException;

class EcoletOAuthClient
{
    public const OAUTH_URL = 'https://panel.ecolet.ro/oauth/authorize';
    public const OAUTH_TEST_URL = 'https://staging.ecolet.ro/oauth/authorize';
    public const OAUTH_TOKEN_URL = 'https://panel.ecolet.ro/api/v1/oauth/token';
    public const OAUTH_TEST_TOKEN_URL = 'https://staging.ecolet.ro/api/v1/oauth/token';
    private Client $httpClient;
    private string $clientId;
    private string $clientSecret;
    private string $redirectUri;
    private string $authUrl;
    private string $tokenUrl;

    /**
     * Initializes the OAuth client with configuration and test mode settings.
     * @param array $config Configuration array containing 'client_id', 'client_secret', and 'redirect_uri'.
     * @param bool $test Whether to use test environment URLs; defaults to false.
     * @return void
     */
    public function __construct(array $config, bool $test = false)
    {
        $this->httpClient = new Client([
            'base_uri' => $test ? self::OAUTH_TEST_URL : self::OAUTH_URL,
            'timeout'  => 10.0,
        ]);

        $this->clientId = $config['client_id'];
        $this->clientSecret = $config['client_secret'];
        $this->redirectUri = $config['redirect_uri'];
        $this->authUrl = $test ? self::OAUTH_TEST_URL : self::OAUTH_URL;
        $this->tokenUrl = $test ? self::OAUTH_TEST_TOKEN_URL : self::OAUTH_TOKEN_URL;
    }

    /**
     * Generates the authorization URL for redirecting the user to the OAuth2 authorization endpoint.
     * @param string $state A randomly generated state parameter to prevent CSRF attacks and maintain the session state.
     * @return string The constructed authorization URL with query parameters.
     */
    public function getAuthorizationUrl(string $state = ''): string
    {
        return sprintf(
            "%s?client_id=%s&redirect_uri=%s&response_type=code&state=%s",
            $this->authUrl,
            urlencode($this->clientId),
            urlencode($this->redirectUri),
            urlencode($state)
        );
    }

    /**
     * Retrieves an access token from the authorization server using an authorization code.
     * @param string $code The authorization code obtained from the user during the authorization flow.
     * @return ?Token The access token response, or null if the request failed or no response was received.
     * @throws JsonException
     * @throws ResponseException
     */
    public function getAccessToken(string $code): ?Token
    {
        return $this->sendRequest([
            'grant_type' => 'authorization_code',
            'client_id' => $this->clientId,
            'client_secret' => $this->clientSecret,
            'redirect_uri' => $this->redirectUri,
            'code' => $code,
        ]);
    }

    /**
     * Retrieves an access token using the password grant type.
     *
     * @param string $username The username for authentication.
     * @param string $password The password for authentication.
     * @return ?Token The access token response, or null if the request failed.
     * @throws JsonException
     * @throws ResponseException
     */
    public function getAccessTokenWithPassword(string $username, string $password): ?Token
    {
        return $this->sendRequest([
            'grant_type' => 'password',
            'client_id' => $this->clientId,
            'client_secret' => $this->clientSecret,
            'username' => $username,
            'password' => $password,
            'scope' => '',
        ]);
    }

    /**
     * Refreshes an access token using a refresh token.
     *
     * @param string $refreshToken The refresh token used to obtain a new access token.
     * @return ?Token The newly obtained token or null if the request failed.
     * @throws JsonException
     * @throws ResponseException
     */
    public function refreshAccessToken(string $refreshToken): ?Token
    {
        return $this->sendRequest([
            'grant_type' => 'refresh_token',
            'refresh_token' => $refreshToken,
            'client_id' => $this->clientId,
            'client_secret' => $this->clientSecret,
            'scope' => '',
        ]);
    }

    /**
     * Sends a POST request to the token endpoint with the provided data and returns a Token object if successful.
     *
     * @param array $data The form data to include in the request, such as client_id, client_secret, and grant_type.
     * @return ?Token The decoded Token object from the response, or null if the request fails or the response is invalid.
     * @throws JsonException
     * @throws ResponseException If a Guzzle HTTP exception occurs or the response body is invalid JSON.
     */
    private function sendRequest(array $data): ?Token
    {
        try {
            $response = $this->httpClient->post($this->tokenUrl, [
                'form_params' => $data,
                'headers' => [
                    'Accept' => 'application/json',
                    'Content-Type' => 'application/x-www-form-urlencoded'
                ]
            ]);

            $responseData = json_decode($response->getBody()->getContents(), true, 512, JSON_THROW_ON_ERROR);
            return new Token($responseData);

        } catch (GuzzleException $e) {
            throw new ResponseException($e->getMessage());
        }
    }
}