<?php
namespace Misuzu\Twitter;

use Index\XString;
use Index\Serialisation\Serialiser;

class TwitterAuthorisation {
    private const AUTHORIZE = 'https://twitter.com/i/oauth2/authorize';

    private const STATE_RNG_LENGTH = 16;
    private const STATE_EPOCH = 1661126400;
    private const STATE_TOLERANCE = 5 * 60;
    private const VERIFIER_LENGTH = 48;

    private TwitterClientId $clientId;
    private array $scope;
    private string $redirect;
    private string $state;
    private string $verifier;
    private string $verifierHash;

    public function __construct(TwitterClientId $clientId, array $scope, string $redirect) {
        $this->clientId = $clientId;
        $this->scope = $scope;
        $this->redirect = $redirect;

        $this->state = self::generateState($clientId);
        [$this->verifier, $this->verifierHash] = self::generateVerifier();
    }

    public function getClientId(): TwitterClientId {
        return $this->clientId;
    }

    public function getScope(): array {
        return $this->scope;
    }

    public function getRedirectUri(): string {
        return $this->redirect;
    }

    public function getState(): string {
        return $this->state;
    }

    public function getVerifier(): string {
        return $this->verifier;
    }

    public function getVerifierHash(): string {
        return $this->verifierHash;
    }

    public function getUri(): string {
        return self::AUTHORIZE . '?' . http_build_query([
            'response_type' => 'code',
            'client_id' => $this->clientId->getClientId(),
            'redirect_uri' => $this->redirect,
            'scope' => implode(' ', $this->scope),
            'state' => $this->state,
            'code_challenge' => $this->verifierHash,
            'code_challenge_method' => 'S256',
        ], '', null, PHP_QUERY_RFC3986);
    }

    public static function generateVerifier(): array {
        $verifier = XString::random(self::VERIFIER_LENGTH);
        return [
            $verifier,
            Serialiser::uriBase64()->serialise(hash('sha256', $verifier, true)),
        ];
    }

    private static function currentStateTime(): int {
        return time() - self::STATE_EPOCH;
    }

    public static function generateState(TwitterClientId $clientId): string {
        $rng = XString::random(self::STATE_RNG_LENGTH);
        $time = self::currentStateTime();

        $string = $rng . ':' . (string)$time;
        $hash = hash_hmac('sha256', $string, $clientId->getClientSecret(), true);

        $time = Serialiser::base62()->serialise($time);
        $hash = Serialiser::uriBase64()->serialise($hash);

        return $rng . '.' . $time  . '.' . $hash;
    }

    public static function verifyState(TwitterClientId $clientId, string $state): bool {
        $parts = explode('.', $state, 4);
        if(count($parts) !== 3)
            return false;

        $rng = $parts[0];
        if(strlen($rng) !== self::STATE_RNG_LENGTH)
            return false;

        $currentTime = self::currentStateTime();
        $time = Serialiser::base62()->deserialise($parts[1]);
        if($currentTime < $time || $currentTime >= ($time + self::STATE_TOLERANCE))
            return false;

        $hash = Serialiser::uriBase64()->deserialise($parts[2]);
        if(strlen($hash) !== 32)
            return false;

        $string = $rng . ':' . (string)$time;
        $realHash = hash_hmac('sha256', $string, $clientId->getClientSecret(), true);

        return hash_equals($realHash, $hash);
    }
}
