<?php

namespace RAP;

use \Firebase\JWT\JWT;

class OAuth2RequestHandler {

    private $locator;

    public function __construct(\RAP\Locator $locator) {
        $this->locator = $locator;
    }

    public function handleAuthorizeRequest($params) {

        if ($params['client_id'] === null) {
            throw new BadRequestException("Client id is required");
        }

        if ($params['redirect_uri'] === null) {
            throw new BadRequestException("Redirect URI is required");
        }

        $client = $this->locator->getOAuth2ClientDAO()->getOAuth2ClientByClientId($params['client_id']);
        if ($client === null) {
            throw new BadRequestException("Invalid client id: " . $params['client_id']);
        }
        if ($client->redirectUrl !== $params['redirect_uri']) {
            throw new BadRequestException("Invalid client redirect URI: " . $params['redirect_uri']);
        }

        $alg = $params['alg'];
        if ($alg === null) {
            $alg = "RS256";
        }

        $state = $params['state'];
        if ($state === null) {
            throw new BadRequestException("State is required");
        }

        // Storing OAuth2 data in session
        $oauth2Data = new \RAP\OAuth2Data();
        $oauth2Data->clientId = $client->client;
        $oauth2Data->redirectUrl = $client->redirectUrl;
        $oauth2Data->state = $state;

        $scope = $params['scope'];
        if ($scope !== null) {
            $oauth2Data->scope = explode(' ', $scope);
        }

        $session = $this->locator->getSession();
        $session->setOAuth2Data($oauth2Data);
    }

    public function getCodeResponseUrl(): string {

        $session = $this->locator->getSession();

        $accessToken = new \RAP\AccessToken();
        $accessToken->code = base64_encode(bin2hex(openssl_random_pseudo_bytes(64)));
        $accessToken->token = base64_encode(bin2hex(openssl_random_pseudo_bytes(128)));
        $accessToken->userId = $session->getUser()->id;
        $accessToken->clientId = $session->getOAuth2Data()->clientId;
        $accessToken->redirectUri = $session->getOAuth2Data()->redirectUrl;
        $accessToken->scope = $session->getOAuth2Data()->scope;

        $this->locator->getAccessTokenDAO()->createAccessToken($accessToken);

        $state = $session->getOAuth2Data()->state;

        $redirectUrl = $session->getOAuth2Data()->redirectUrl
                . '?code=' . $accessToken->code . '&scope=profile&state=' . $state;

        return $redirectUrl;
    }

    public function handleAccessTokenRequest($params): array {

        $this->validateAccessTokenRequest($params);

        $accessToken = $this->locator->getAccessTokenDAO()->retrieveAccessTokenFromCode($params['code']);

        if ($accessToken === null) {
            throw new BadRequestException("No token for given code");
        }

        if ($accessToken->redirectUri !== $params['redirect_uri']) {
            throw new BadRequestException("Invalid redirect URI: " . $params['redirect_uri']);
        }

        $result = [];
        $result['access_token'] = $accessToken->token;
        $result['token_type'] = 'Bearer';
        $result['expires_in'] = $this->getExpiresIn($accessToken);

        if ($accessToken->scope !== null && in_array('openid', $accessToken->scope)) {
            $result['id_token'] = $this->locator->getIdTokenBuilder()->getIdToken($accessToken);
        }

        return $result;
    }

    private function validateAccessTokenRequest($params) {

        if ($params['grant_type'] === null) {
            throw new BadRequestException("grant_type is required");
        } else if ($params['grant_type'] !== 'authorization_code') {
            throw new BadRequestException("grant_type must be authorization_code");
        }

        if ($params['code'] === null) {
            throw new BadRequestException("code id is required");
        }

        if ($params['redirect_uri'] === null) {
            throw new BadRequestException("Redirect URI is required");
        }

        // Note: theorically the standard wants also the client_id here,
        // however some clients don't send it
    }

    public function handleCheckTokenRequest($token): array {

        if (!isset($_POST['token'])) {
            throw new BadRequestException("Access token id is required");
        }

        $accessToken = $this->locator->getAccessTokenDAO()->getAccessToken($token);
        $user = $this->locator->getUserDAO()->findUserById($accessToken->userId);

        $result = [];
        $result['exp'] = $this->getExpiresIn($accessToken);
        $result['user_name'] = $user->id;
        $result['client_id'] = $accessToken->clientId;

        if ($accessToken->scope !== null) {
            $result['scope'] = $accessToken->scope;
            if (in_array('openid', $accessToken->scope)) {
                $result['id_token'] = $this->locator->getIdTokenBuilder()->getIdToken($accessToken);
            }
        }

        return $result;
    }

    private function getExpiresIn(AccessToken $accessToken) {
        $expTime = strtotime($accessToken->expirationTime);
        $now = time();
        return $expTime - $now;
    }

    public function validateToken(): void {
        $headers = apache_request_headers();

        if (!isset($headers['Authorization'])) {
            throw new BadRequestException("Missing Authorization header");
        }

        $authorizationHeader = explode(" ", $headers['Authorization']);
        if ($authorizationHeader[0] === "Bearer") {
            $bearer_token = $authorizationHeader[1];
        } else {
            throw new BadRequestException("Invalid token type");
        }

        $accessToken = $this->locator->getAccessTokenDAO()->getAccessToken($bearer_token);
        if ($accessToken === null) {
            $this->attemptJWTTokenValidation($bearer_token);
        } else if ($accessToken->expired) {
            throw new UnauthorizedException("Access token is expired");
        }
    }

    private function attemptJWTTokenValidation($jwt): void {

        $jwtParts = explode('.', $jwt);
        if (count($jwtParts) === 0) {
            throw new UnauthorizedException("Invalid token");
        }

        $header = JWT::jsonDecode(JWT::urlsafeB64Decode($jwtParts[0]));
        if (!isset($header->kid)) {
            throw new UnauthorizedException("Invalid token: missing kid in header");
        }

        $keyPair = $this->locator->getJWKSDAO()->getRSAKeyPairById($header->kid);
        if ($keyPair === null) {
            throw new UnauthorizedException("Invalid kid: no key found");
        }

        try {
            JWT::decode($jwt, $keyPair->publicKey, [$keyPair->alg]);
        } catch (\Firebase\JWT\ExpiredException $ex) {
            throw new UnauthorizedException("Access token is expired");
        }
    }

}