<?php
/**
 * @author      Alex Bilbie <hello@alexbilbie.com>
 * @copyright   Copyright (c) Alex Bilbie
 * @license     http://mit-license.org/
 *
 * @link        https://github.com/thephpleague/oauth2-server
 */

namespace Docuform\Api\Authentication\Repositories;

use DI\DependencyException;
use DI\NotFoundException;
use Docuform\Database\DbException;
use Exception;
use League\OAuth2\Server\Entities\RefreshTokenEntityInterface;
use League\OAuth2\Server\Repositories\RefreshTokenRepositoryInterface;
use Docuform\Api\Authentication\Entities\RefreshTokenEntity;
use DI\Container;
use Docuform\Database\DbPDO;
use Docuform\Lib\ApiDateTime;
use Docuform\Api\Exception\ApiHttpAuthException;
use Docuform\Api\Authentication\AuthApi;

// TODO: Get from ApiClients-Table
global $g_bAllowMultiConnects;
$g_bAllowMultiConnects = true;

class RefreshTokenRepository implements RefreshTokenRepositoryInterface
{
  private Container $m_container;

  private DbPDO $m_database;

  /**
   * @throws DependencyException
   * @throws NotFoundException
   */
  public function __construct(Container $container)
  {
    $this->m_container = $container;
    $this->m_database =  $container->get('database');
  }

  /**
   * {@inheritdoc}
   * @throws Exception
   */
  public function persistNewRefreshToken(RefreshTokenEntityInterface $refreshTokenEntity): void
  {
    global $g_bAllowMultiConnects;

    // persist the refresh token in a database
    $dCreated = new ApiDateTime;

    /** @var AuthApi $authApi */
    $authApi = $this->m_container->get('RestApi');

    if ($this->m_database != null)
    {
      $accessToken = $refreshTokenEntity->getAccessToken();
      $client = $accessToken->getClient();
      $clientApiKey = $client->getIdentifier();
      $userUuid = $accessToken->getUserIdentifier();

      $scopearr = array();
      foreach ($accessToken->getScopes() as $scope)
      {
        $scopearr[] = $scope->getIdentifier();
      }
      $scopes = implode (' ' , $scopearr);

      $deviceId = (string) $this->m_container->get('apiclientdeviceid');
      $refreshTokenId = $refreshTokenEntity->getIdentifier();

      $dExpiresUnix = $refreshTokenEntity->getExpiryDateTime()->getTimestamp();
      // Convert expiration time to UTC
      $dExpires = new ApiDateTime;
      $dExpires->setTimestamp($dExpiresUnix);

      $userId = !empty($userUuid) ? $authApi->getUserIdFromUuid($userUuid) : null;

      if ($userId !== false)
      {
        $clientId = !empty($clientApiKey) ?
          (int)$this->m_database->getDbValue('ACCAPICLIENTS', 'Id', 'ApiKey=?', [$clientApiKey]) : 0;

        if ($clientId > 0)
        {
          $dbCreated = $this->m_database->DbDate($dCreated);
          $dbExpires = $this->m_database->DbDate($dExpires);

          // Cleanup
          if ($g_bAllowMultiConnects)
          {
            $params = [$this->m_database->NowDb(), $clientId, $deviceId];

            $sql = 'DELETE FROM ACCAPITOKENS WHERE ?>Expires OR (ApiClientId=? AND ApiDeviceId=?';
          }
          else
          {
            $params = [$this->m_database->NowDb(), $clientId];

            $sql = 'DELETE FROM ACCAPITOKENS WHERE ?>Expires OR (ApiClientId=?';
          }

          if ($userId > 0)
          {
            $sql .= ' AND UserId=?';
            $params[] = $userId;
          }

          $sql .= ")";

          $this->m_database->Execute($sql, $params);

          // Insert new refresh token
          $sql = 'INSERT INTO ACCAPITOKENS (ApiClientId, ApiDeviceId, UserId, RefreshTokenId, Scopes, Created, Expires)'.
            ' VALUES (?,?,?,?,?,?,?)';

          $params = [$clientId, $deviceId, $userId, $refreshTokenId, $scopes, $dbCreated, $dbExpires];

          // TODO: throw UniqueTokenIdentifierConstraintViolationException
          if ($this->m_database->Execute($sql, $params) === false)
          {
            throw new ApiHttpAuthException(500, 'access_token_duplicate',
              'Could not create unique access token identifier', '',
              $this->m_container->get('redirect_uri'), $this->m_container->get('logger'),
              'Database error');
          }

          $this->m_container->set('RefreshTokenId', $refreshTokenId);
        }
        else
        {
          throw new ApiHttpAuthException(500, 'access_token_duplicate',
            'Could not create unique access token identifier', '',
            $this->m_container->get('redirect_uri'), $this->m_container->get('logger'),
            'Invalid client');
        }
      }
      else
      {
        throw new ApiHttpAuthException(500, 'access_token_duplicate',
          'Could not create unique access token identifier', '',
          $this->m_container->get('redirect_uri'), $this->m_container->get('logger'),
          'Invalid user');
      }
    }
    else
    {
      throw new ApiHttpAuthException(500, 'access_token_duplicate', 'Could not create unique access token identifier',
        '',
        $this->m_container->get('redirect_uri'), $this->m_container->get('logger'),
        'No database');
    }
  }

  /**
   * @param string $tokenId
   * @return void
   * @throws DbException
   */
  public function revokeRefreshToken(string $tokenId): void
  {
    // revoke the refresh token in a database
    $sql = 'DELETE FROM ACCAPITOKENS WHERE RefreshTokenId=?';

    $this->m_database->Execute($sql, array($tokenId));
  }

  /**
   */
  private function cleanupExpiredRefreshTokens(): void
  {
    try
    {
      $params = [$this->m_database->NowDb()];

      $sql = 'DELETE FROM ACCAPITOKENS WHERE ?>Expires';

      $this->m_database->Execute($sql, $params);
    }
    catch (DbException|Exception $e)
    {
    }
  }

  /**
   * @param string $tokenId
   * @return bool
   * @throws DependencyException
   * @throws NotFoundException
   */
  public function isRefreshTokenRevoked(string $tokenId): bool
  {
    /** @var string $clientDeviceId */
    $clientDeviceId = $this->m_container->get('apiclientdeviceid');

    $this->cleanupExpiredRefreshTokens ();

    $pkId = (int)$this->m_database->getDbValue('ACCAPITOKENS', 'pkId', 'RefreshTokenId=? AND ApiDeviceId=?',
      [$tokenId, $clientDeviceId]);

    return $pkId === 0;
  }

  /**
   * @return RefreshTokenEntity|RefreshTokenEntityInterface|null
   */
  public function getNewRefreshToken(): RefreshTokenEntity|RefreshTokenEntityInterface|null
  {
    return new RefreshTokenEntity();
  }
}
