<?php

namespace Authstack\OpenIDConnect\Model\Protocol\Flow;

use Assert\Assertion;
use Illuminate\Http\Request;
use Authstack\OpenIDConnect\Client;
use Assert\InvalidArgumentException;
use Authstack\OpenIDConnect\Provider;
use Illuminate\Http\RedirectResponse;
use function Authstack\OpenIDConnect\base64_urlencode;

class AuthorizationCode extends AbstractFlow implements FlowInterface
{
    protected $query;
    protected $response;
    protected $to;
    protected $pkce = false;
    protected $code_verifier;
    protected $code_challenge;

    /**
     * @param bool $which
     * @return $this
     */
    public function setPKCE(bool $which = false)
    {
        $this->pkce = $which;

        return $this;
    }

    /**
     * @return bool
     */
    public function usesPKCE(): bool
    {
        return $this->pkce;
    }

    /**
     * @param bool $allow
     * @return AuthorizationCode
     */
    public function setVerifySSL(bool $allow = false): self
    {
        $this->verifySSL = $allow;

        return $this;
    }

    /**
     * @return string
     */
    public function getCodeChallengeMethod(): string
    {
        return 'S256';
    }

    /**
     * @param string $verifier
     * @return AuthorizationCode
     */
    public function setCodeVerifier(string $verifier): self
    {
        $this->code_verifier = $verifier;

        return $this;
    }

    /**
     * @return string
     */
    public function getCodeVerifier(): string
    {
        return $this->code_verifier;
    }

    /**
     * @return AuthorizationCode
     */
    public function randomizeCodeVerifier(): self
    {
        $this->code_verifier = hash('sha256', random_bytes(32));

        return $this;
    }

    /**
     * @return string
     */
    public function getOrCreateCodeVerifier(): string
    {
        if(empty($this->code_verifier))
        {
            $this->randomizeCodeVerifier();
        }

        return $this->code_verifier;
    }

    /**
     * @param string $challenge
     */
    public function setCodeChallenge(string $challenge)
    {
        $this->code_challenge = $challenge;
    }

    /**
     * @param string $code_verifier
     * @return string
     */
    public function getCodeChallenge(string $code_verifier): string
    {
        return base64_urlencode(hash('sha256', $code_verifier));
    }

    /**
     * @param Provider $provider
     * @param Client $client
     * @return FlowInterface
     */
    public function build(Provider $provider, Client $client): FlowInterface
    {
        $this->query = $this->buildQuery($client);

        $this->to = sprintf("%s?%s", $provider->getAuthEndpoint(), http_build_query($this->query, null, '&', PHP_QUERY_RFC3986));

        $this->response = new RedirectResponse($this->to, 302);

        return $this;
    }

    /**
     * @param Provider $provider
     * @param Client $client
     * @return RedirectResponse
     */
    public function authenticate(Provider $provider, Client $client): RedirectResponse
    {
        if(!$this->response instanceof RedirectResponse)
        {
            $this->build($provider, $client);
        }

        return $this->response;
    }

    /**
     *  In this flow type, we need to ask for id_token using the code we obtained.
     *
     * @param Provider $provider
     * @param Client $client
     * @param Request $request
     * @return array
     * @throws InvalidArgumentException
     */
    public function consume(Provider $provider, Client $client, Request $request): array
    {
        $http = $this->getHttpClient();

        $code = $request->input('code');

        Assertion::string($code, 'Invalid value received for "code"');

        // Build the exchange params. Takes care of PKCE etc.
        $params = $this->buildExchangeParams($client, $code);

        // POST to the OP token endpoint. Uses application/x-www-form-urlencoded
        $result = $http->request('POST', $provider->getTokenEndpoint(), ['form_params' => $params]);

        // We should get an array with id_token (must), access_token (optional) and refresh_token (optional)
        // What we do at this point is return the result and leave the developer to take care of parsing / verifying
        // the tokens. This library contains tools that do so, but the AuthorizationCode flow won't be doing it automatically
        $json = @json_decode($result->getBody()->getContents(), true);

        Assertion::eq(JSON_ERROR_NONE, json_last_error(), 'An error occurred while decoding JSON response: ' . json_last_error_msg());

        return $json;
    }

    /**
     * @return array
     */
    public function getQuery(): array
    {
        return $this->query;
    }

    /**
     * @return RedirectResponse
     */
    public function getResponse(): RedirectResponse
    {
        return $this->response;
    }

    /**
     * @return null|string
     */
    public function getRedirectURI(): ?string
    {
        return $this->to;
    }

    /**
     * @param Client $client
     * @return array
     */
    protected function buildQuery(Client $client): array
    {
        $query = $this->getResponseType();

        $query['client_id'] = $client->getClientID();
        $query['scope'] = $client->getScopes();
        $query['redirect_uri'] = $client->getRedirectURI();
        $query['nonce'] = $client->getNonce();

        if($this->usesPKCE())
        {
            $code_verifier = $this->getOrCreateCodeVerifier();

            $query['code_challenge'] = $code_verifier;
            $query['code_challenge_method'] = $this->getCodeChallengeMethod();
        }

        // Add the parameters from the client now
        if($client->hasQueryParams())
        {
            // We merge arrays by having $query being the important one, so that $client keys don't overwrite $query ones
            // but vice versa
            $query = array_merge($client->getQueryParams(), $query);
        }

        return $query;
    }

    /**
     * @return array
     */
    protected function getResponseType(): array
    {
        return [
            'response_type' => 'code'
        ];
    }

    /**
     * @param Client $client
     * @param string $code
     * @return array
     */
    protected function buildExchangeParams(Client $client, string $code): array
    {
        $params = [
            'grant_type' => 'authorization_code',
            'code' => $code,
            'client_id' => $client->getClientID(),
            'client_secret' => $client->getClientSecret(),
            'redirect_uri' => $client->getRedirectURI()
        ];

        if($this->usesPKCE())
        {
            $params['code_verifier'] = $this->getCodeVerifier();
        }

        return $params;
    }
}