<?php

namespace RAP;

use \Firebase\JWT\JWT;

class TokenBuilder {

    private $locator;

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

    public function getIdToken(AccessTokenData $tokenData, \Closure $jwtCustomizer = null): string {

        $keyPair = $this->locator->getJWKSDAO()->getNewestKeyPair();

        $payload = $this->createIdTokenPayloadArray($tokenData, $jwtCustomizer);

        return JWT::encode($payload, $keyPair->privateKey, $keyPair->alg, $keyPair->keyId);
    }

    private function createIdTokenPayloadArray(AccessTokenData $tokenData, \Closure $jwtCustomizer = null) {

        $user = $this->locator->getUserDAO()->findUserById($tokenData->userId);

        $payloadArr = array(
            'iss' => $this->locator->config->jwtIssuer,
            'sub' => strval($user->id),
            'iat' => intval($tokenData->creationTime),
            'exp' => intval($tokenData->expirationTime),
            'name' => $user->getCompleteName(),
            'aud' => $tokenData->clientId
        );

        if (in_array("email", $tokenData->scope)) {
            $payloadArr['email'] = $user->getPrimaryEmail();
        }
        if (in_array("profile", $tokenData->scope)) {
            $payloadArr['given_name'] = $user->getName();
            $payloadArr['family_name'] = $user->getSurname();
            if ($user->getInstitution() !== null) {
                $payloadArr['org'] = $user->getInstitution();
            }
        }

        if ($jwtCustomizer !== null) {
            // Add additional custom claims
            $jwtCustomizer($payloadArr);
        }

        return $payloadArr;
    }

    public function getAccessToken(AccessTokenData $tokenData, \Closure $jwtCustomizer = null): string {

        $keyPair = $this->locator->getJWKSDAO()->getNewestKeyPair();

        $user = $this->locator->getUserDAO()->findUserById($tokenData->userId);
        if ($user === null) {
            // CLI client
            $sub = $tokenData->clientId;
        } else {
            $sub = $user->id;
        }

        $payload = array(
            'iss' => $this->locator->config->jwtIssuer,
            'sub' => strval($sub),
            'iat' => intval($tokenData->creationTime),
            'exp' => intval($tokenData->expirationTime),
            'aud' => $this->getAudience($tokenData),
            'scope' => implode(' ', $tokenData->scope)
        );
        if ($jwtCustomizer !== null) {
            // Add additional custom claims
            $jwtCustomizer($payload);
        }

        return JWT::encode($payload, $keyPair->privateKey, $keyPair->alg, $keyPair->keyId);
    }

    private function getAudience(AccessTokenData $tokenData) {

        if ($tokenData->audience !== null) {
            return $this->getAudienceClaim($tokenData->audience);
        }

        $client = $this->locator->getBrowserBasedOAuth2ClientById($tokenData->clientId, true);
        if ($client === null) {
            // CLI client without audience
            return null;
        }

        $audiences = [$tokenData->clientId];

        if ($client->scopeAudienceMap !== null) {
            foreach ($tokenData->scope as $scope) {
                if (array_key_exists($scope, $client->scopeAudienceMap)) {
                    $audience = ((array) $client->scopeAudienceMap)[$scope];
                    if (!in_array($audience, $audiences)) {
                        array_push($audiences, $audience);
                    }
                }
            }
        }

        return $this->getAudienceClaim($audiences);
    }

    private function getAudienceClaim($audiences) {
        if (count($audiences) === 1) {
            // according to RFC 7519 audience can be a single value or an array
            return $audiences[0];
        }
        return $audiences;
    }

    public function generateToken(array $claims) {

        $iat = time();

        // basic payload
        $payload = array(
            'iss' => $this->locator->config->jwtIssuer,
            'iat' => $iat
        );

        // copy claims passed as parameter
        foreach ($claims as $key => $value) {
            $payload[$key] = $value;
        }

        // set expiration claim if it doesn't exist
        if (!array_key_exists('exp', $payload)) {
            $payload['exp'] = $iat + 3600;
        }

        $keyPair = $this->locator->getJWKSDAO()->getNewestKeyPair();
        return JWT::encode($payload, $keyPair->privateKey, $keyPair->alg, $keyPair->keyId);
    }

    /**
     * @param int $lifespan in hours
     * @param string $audience target service
     */
    public function generateNewToken(string $subject, int $lifespan, string $audience) {
        $keyPair = $this->locator->getJWKSDAO()->getNewestKeyPair();

        $iat = time();
        $exp = $iat + $lifespan * 3600;

        $payload = array(
            'iss' => $this->locator->config->jwtIssuer,
            'sub' => strval($subject),
            'iat' => $iat,
            'exp' => $exp,
            'aud' => $audience
        );

        $conf = $this->getTokenIssuerConfig($audience);
        if (property_exists($conf, 'aud')) {
            $payload['aud'] = $conf->aud;
        }
        if (property_exists($conf, 'scope')) {
            $payload['scope'] = $conf->scope;
        }

        return JWT::encode($payload, $keyPair->privateKey, $keyPair->alg, $keyPair->keyId);
    }

    private function getTokenIssuerConfig($audience) {
        foreach ($this->locator->config->tokenIssuer->services as $service) {
            if ($service->id === $audience) {
                return $service;
            }
        }
        throw new \Exception("Unable to find configuration for " . $audience);
    }

}