<?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 Docuform\Database\DbPDO;
use League\OAuth2\Server\Repositories\ClientRepositoryInterface;
use Docuform\Api\Authentication\Entities\ApiClientEntity;
use League\OAuth2\Server\Entities\ClientEntityInterface;
use DI\Container;
use Psr\Log\LoggerInterface;
use function Docuform\Api\Authentication\DecryptAES128;

class ApiClientRepository implements ClientRepositoryInterface
{
  private Container $m_container;

  /**
   * @param Container $container
   */
  public function __construct(Container $container)
  {
    $this->m_container = $container;
  }


  /**
   * @param string $clientIdentifier
   *
   * @return bool|array
   *
   * @throws DependencyException
   * @throws NotFoundException
   * @throws DbException
   */
  private function readApiClientFromDb (string $clientIdentifier): bool|array
  {
    $clientData = false;

    /** @var DbPDO $database */
    $database = $this->m_container->get('database');

    $sql = "SELECT * FROM ACCAPICLIENTS WHERE IsActive=1 AND ApiKey='" . $clientIdentifier . "'";

    $Cursor = null;
    $Row = $database->FetchFirst($Cursor, $sql);

    if ($Row !== false)
    {
      $clientData = [];
      $clientData['identifier'] = trim($Row['ApiKey']);

      if (!empty(trim($Row['ApiSecret'])))
      {
        $apiSecret = DecryptAES128(trim($Row['ApiSecret']));
      }
      else
      {
        $apiSecret = '';
      }

      $clientData['name']   = trim($Row['Name']);
      $clientData['secret'] = $apiSecret;

      $grant_types = explode (' ', trim($Row['GrantTypes']));
      $clientData['pkce_extension'] = false;

      $pkce = array_search('authorization_code_pkce', $grant_types);
      if ($pkce !== FALSE)
      {
        $grant_types[$pkce] = 'authorization_code';
        $clientData['pkce_extension'] = true;
      }

      $clientData['grant_types']     = implode(' ', $grant_types);
      $clientData['scopes']          = trim($Row['Scopes']);
      $clientData['redirect_uri']    = trim($Row['RedirectUri']);
      $clientData['is_confidential'] = trim($Row['IsConfidential']) == 1 ? true : false;
    }

    if ($Cursor !== null)
    {
      $database->FetchClose($Cursor);
    }

    return $clientData;
  }


  /**
   * @param string $clientIdentifier
   *
   * @return ClientEntityInterface|null
   * @throws DbException
   * @throws DependencyException
   * @throws NotFoundException
   */
  public function getClientEntity(string $clientIdentifier): ?ClientEntityInterface
  {
    $client = null;

    $clientData = $this->readApiClientFromDb($clientIdentifier);

    if ($clientData !== false)
    {
      $client = new ApiClientEntity();

      $client->setIdentifier($clientIdentifier);
      $client->setName($clientData['name']);
      $client->setRedirectUri($clientData['redirect_uri']);
      $client->setConfidential($clientData['is_confidential']);
      $client->setScopes($clientData['scopes']);
      $client->setPkceExtension($clientData['pkce_extension']);
    }

    return $client;
  }


  /**
   * @param string      $clientIdentifier
   * @param string|null $clientSecret
   * @param string|null $grantType
   *
   * @return bool
   *
   * @throws DbException
   */
  public function validateClient(string $clientIdentifier, ?string $clientSecret, ?string $grantType): bool
  {
    $bValidated = false;

    /** @var LoggerInterface $logger */
    try
    {
      $logger = $this->m_container->get('logger');

      $clientData = $this->readApiClientFromDb($clientIdentifier);

      if ($clientData !== false)
      {
        // Check if grantType is allowed
        $allowedGrantTypes = explode(' ', $clientData['grant_types']);

        // password not allowed for public clients
        if ($clientData['is_confidential'] === false)
        {
          $allowedGrantTypes = array_diff($allowedGrantTypes, ["password"]);
        }

        if (in_array($grantType, $allowedGrantTypes, true) === false)
        {
          $logger->error("Grant type '$grantType' not allowed for client '$clientIdentifier'");
          return false;
        }

        $bValidated = $this->checkSecret($clientIdentifier, $clientData, $allowedGrantTypes, $grantType, $clientSecret);

        if (!$bValidated)
        {
          if (empty($clientSecret))
          {
            $logger->error("Client '$clientIdentifier': client_secret missing");
          }
          else
          {
            $logger->error("Client '$clientIdentifier': Wrong client_secret");
          }
        }
      }
    }
    catch (DependencyException|NotFoundException $e)
    {
    }

    return $bValidated;
  }


  /**
   * @param string      $clientIdentifier
   * @param array       $clientdata
   * @param array       $allowedGrantTypes
   * @param string      $grantType
   * @param string|null $clientSecret
   *
   * @return bool
   *
   * @throws DependencyException
   * @throws NotFoundException
   */
  private function checkSecret(string $clientIdentifier, array $clientdata, array $allowedGrantTypes, string $grantType, ?string $clientSecret = null): bool
  {
    $bInvalidSecret = false;

    if ($grantType === 'authorization_code')
    {
      if (in_array('authorization_code', $allowedGrantTypes, true))
      {
        $bInvalidSecret = empty($clientdata['pkce_extension']) && $clientSecret !== $clientdata['secret'];
      }
    }
    else
    {
      $bInvalidSecret = $clientdata['is_confidential'] === true && $clientSecret !== $clientdata['secret'];
    }

    if ($bInvalidSecret)
    {
      /** @var LoggerInterface $logger */
      $logger = $this->m_container->get('logger');

      if (empty($clientSecret))
      {
        $logger->error("Client '$clientIdentifier': client_secret missing");
      }
      else
      {
        $logger->error("Client '$clientIdentifier': Invalid client_secret");
      }
    }

    return !$bInvalidSecret;
  }
}
