<?php namespace RVR\Controller;

use DateTime;
use Firebase\JWT\JWT;
use RVR\Repository\OAuthTokenRepository;
use RVR\Repository\UserRepository;
use RVR\PersistentData\Model\User;
use RVR\Repository\OAuthClientRepository;
use SokoWeb\Interfaces\Response\IContent;
use SokoWeb\Response\JsonContent;

class OAuthController
{
    private OAuthClientRepository $oAuthClientRepository;

    private OAuthTokenRepository $oAuthTokenRepository;

    private UserRepository $userRepository;

    public function __construct()
    {
        $this->oAuthClientRepository = new OAuthClientRepository();
        $this->oAuthTokenRepository = new OAuthTokenRepository();
        $this->userRepository = new UserRepository();
    }

    public function getToken(): ?IContent
    {
        $clientId = \Container::$request->post('client_id');
        $clientSecret = \Container::$request->post('client_secret');
        $code = \Container::$request->post('code');

        if (!$clientId || !$clientSecret || !$code) {
            return new JsonContent([
                'error' => 'An invalid request was made.'
            ]);
        }

        $client = $this->oAuthClientRepository->getByClientId($clientId);
        if ($client === null || $client->getClientSecret() !== $clientSecret) {
            return new JsonContent([
                'error' => 'Client is not authorized.'
            ]);
        }

        $token = $this->oAuthTokenRepository->getByCode($code);
        if ($token === null || $token->getExpiresDate() < new DateTime()) {
            return new JsonContent([
                'error' => 'The provided code is invalid.'
            ]);
        }

        $payload = array_merge([
            'iss' => $_ENV['APP_URL'],
            'iat' => (int)$token->getCreatedDate()->getTimestamp(),
            'nbf' => (int)$token->getCreatedDate()->getTimestamp(),
            'exp' => (int)$token->getExpiresDate()->getTimestamp(),
            'aud' => $clientId,
            'nonce' => $token->getNonce()
        ], $this->getUserInfoInternal(
            $this->userRepository->getById($token->getUserId()),
            $token->getScopeArray())
        );

        $privateKey = file_get_contents(ROOT . '/' . $_ENV['JWT_RSA_PRIVATE_KEY']);
        $jwt = JWT::encode($payload, $privateKey, 'RS256');

        return new JsonContent([
            'access_token' => $token->getAccessToken(),
            'expires_in' => $token->getExpiresDate()->getTimestamp() - (new DateTime())->getTimestamp(),
            'scope' => $token->getScope(),
            'id_token' => $jwt,
            'token_type' => 'Bearer'
        ]);
    }

    public function getUserInfo() : IContent
    {
        $authorization = \Container::$request->header('Authorization');
        if ($authorization === null) {
            return new JsonContent([
                'error' => 'No Authorization header was sent.'
            ]);
        }

        $accessToken = substr($authorization, strlen('Bearer '));
        $token = $this->oAuthTokenRepository->getByAccessToken($accessToken);

        if ($token === null || $token->getExpiresDate() < new DateTime()) {
            return new JsonContent([
                'error' => 'The provided access token is invalid.'
            ]);
        }

        return new JsonContent(
            $this->getUserInfoInternal(
                $this->userRepository->getById($token->getUserId()),
                $token->getScopeArray()
            )
        );
    }

    public function getConfig(): IContent
    {
        return new JsonContent([
            'issuer' => $_ENV['APP_URL'],
            'authorization_endpoint' => \Container::$request->getBase() . \Container::$routeCollection->getRoute('oauth-auth')->generateLink(),
            'token_endpoint' => \Container::$request->getBase() . \Container::$routeCollection->getRoute('oauth-token')->generateLink(),
            'userinfo_endpoint' => \Container::$request->getBase() . \Container::$routeCollection->getRoute('oauth-userinfo')->generateLink(),
            'jwks_uri' => \Container::$request->getBase() . \Container::$routeCollection->getRoute('oauth-certs')->generateLink(),
            'response_types_supported' =>
            [
                'code',
            ],
            'subject_types_supported' =>
            [
                'public',
            ],
            'id_token_signing_alg_values_supported' =>
            [
                'RS256',
            ],
            'scopes_supported' =>
            [
                'openid',
                'email',
                'profile',
            ],
            'token_endpoint_auth_methods_supported' =>
            [
                'client_secret_post',
            ],
            'claims_supported' =>
            [
                'aud',
                'email',
                'exp',
                'full_name',
                'iat',
                'id_number',
                'iss',
                'nickname',
                'phone',
                'picture',
                'sub',
                'username',
            ],
            'code_challenge_methods_supported' =>
            [
                'plain',
                'S256',
            ],
            'grant_types_supported' =>
            [
                'authorization_code',
            ],
        ]);
    }

    public function getCerts(): IContent
    {
        $publicKey = file_get_contents(ROOT . '/' . $_ENV['JWT_RSA_PUBLIC_KEY']);
        $keyInfo = openssl_pkey_get_details(openssl_pkey_get_public($publicKey));

        return new JsonContent(['keys' => [
            [
                'kty' => 'RSA',
                'alg' => 'RS256',
                'use' => 'sig',
                'kid' => '1',
                'n' => str_replace(['+', '/'], ['-', '_'], base64_encode($keyInfo['rsa']['n'])),
                'e' => str_replace(['+', '/'], ['-', '_'], base64_encode($keyInfo['rsa']['e'])),
            ]
        ]]);
    }

    private function getUserInfoInternal(User $user, array $scope): array
    {
        $userInfo = [];
        if (in_array('openid', $scope)) {
            $userInfo['sub'] = (string)$user->getId();
        }
        if (in_array('email', $scope)) {
            $userInfo['email'] = $user->getEmail();
        }
        if (in_array('profile', $scope)) {
            if ($user->getUsername() !== null) {
                $userInfo['preferred_username'] = $user->getUsername();
            }
            $userInfo['name'] = $user->getFullName();
            $userInfo['nickname'] = $user->getNickname();
            $userInfo['phone_number'] = $user->getPhone();
            $userInfo['id_number'] = $user->getIdNumber();
        }
        return $userInfo;
    }
}