<?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\Container;
use DI\DependencyException;
use DI\NotFoundException;
use Docuform\Api\Authentication\Entities\UserEntity;
use Docuform\Api\ApiUser;
use Docuform\Api\Authentication\AuthLdap;
use Docuform\Api\Exception\ApiHttpException;
use Docuform\Database\DbException;
use Docuform\Database\DbPDO;
use Exception;
use Firebase\JWT\JWT;
use Firebase\JWT\Key;
use League\OAuth2\Server\Entities\ClientEntityInterface;
use League\OAuth2\Server\Entities\UserEntityInterface;
use League\OAuth2\Server\Repositories\UserRepositoryInterface;
use Psr\Log\LoggerInterface;
use stdClass;

class UserRepository implements UserRepositoryInterface
{
  private Container $m_container;

  public function __construct($container)
  {
    $this->m_container = $container;
  }

  /**
   * @param string                $username
   * @param string                $password
   * @param string                $grantType
   * @param ClientEntityInterface $clientEntity
   *
   * @return UserEntityInterface|null
   * @throws DbException
   * @throws DependencyException
   * @throws NotFoundException
   */
  public function getUserEntityByUserCredentials(
      string $username,
      string $password,
      string $grantType,
      ClientEntityInterface $clientEntity
  ): ?UserEntityInterface
  {
    $result = null;

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

    /** @var bool $ldapActive */
    $ldapActive = $this->m_container->get('ldapactive');

    /** @var string $authToken */
    try
    {
      $authToken = $this->m_container->get('authToken');
    }
    catch (Exception $e)
    {
      $authToken = '';
    }

    if (!empty($password) || !empty($authToken))
    {
      $bIsUuid = $this->isValidUuid_fsm($username);

      if ($bIsUuid)
      {
        $user = $this->getUserEntityByUuid($username);
      }
      elseif (strtolower($username) === 'serviceaccount' && !empty($password))
      {
        $user = $this->getUserEntityByUuid(ApiUser::serviceaccount_uuid);
        $bIsUuid = true;
      }
      else
      {
        $user = new UserEntity($this->m_container);

        $user->readEntity(['name' => $username]);
      }

      if (!empty($user->id))
      {
        if (!$ldapActive || $bIsUuid || $user->id === 1)
        {
          /** @var DbPDO $database */
          $database = $this->m_container->get('database');

          if (!empty($authToken))
          {
            if (!$this->checkAuthToken($authToken, $user->uuid, $user->name))
            {
              $logger->error('Invalid mcs authorization token');

              return null;
            }
          }

          $doPasswordCheck = !empty($password) || empty($authToken);

          $dbPassword = $database->getNonUtf8Charset() ? mb_convert_encoding($password, 'ISO-8859-1', 'UTF-8') : $password;

          $config = $this->m_container->get('cfg');

          if ($clientEntity->getIdentifier() === 'Es4v45qpySQUhsEB' && !empty($dbPassword) && !empty($password))
          { // MCS Accounting
            $dbPasswordMd5 = md5($dbPassword);
            $cardReaderPassword = $database->getDbValue('ACCSYSTEM', 'sValue', "sSection='CardReader' AND sKey='Password'", [], '');
            $passwordEmptyMd5 = md5('');

            if ($cardReaderPassword !== $passwordEmptyMd5 && $dbPasswordMd5 === $cardReaderPassword)
            {
              $result = $user;
            }
          }

          if (!$doPasswordCheck || (!empty($user->password) && md5($dbPassword) === $user->password) ||
             (!empty($authToken) && !empty($config['mcs_to_fsm_password']) && $password === $config['mcs_to_fsm_password']))
          {
            $result = $user;
          }
          else
          {
            $logger->warning('Invalid password');
          }
        }
        else
        {
          if (AuthLdap::LdapAuthentication($this->m_container, $username, $password))
          {
            $result = $user;
          }
        }

        unset($user->password);
      }
      else
      {
        $logger->warning('User ' . $username . ' not found');
      }
    }

    return $result;
  }


  /**
   * @param string $authToken
   * @param string|null $userUuid
   * @param string|null $name
   * @return bool
   * @throws DependencyException
   * @throws NotFoundException
   */
  public function checkAuthToken (string $authToken, string|null $userUuid=null, string|null $name=null): bool
  {
    $result = false;

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

    if ($authToken !== '?')
    {
      $config = $this->m_container->get('cfg');

      if (empty($config['mcs_to_fsm_public_keyfile']) || !file_exists($config['mcs_to_fsm_public_keyfile']))
      {
        throw new ApiHttpException(null, 500, 'Public MCS key file missing');
      }

      $publicKey = file_get_contents($config['mcs_to_fsm_public_keyfile']);

      try
      {
        JWT::$leeway = 10*60;
        $tokenData = JWT::decode($authToken, new Key($publicKey, 'RS256'));
      }
      catch (Exception)
      {
        $tokenData = new stdClass();
      }

      if (!empty($tokenData->sub))
      {
        if (!empty($userUuid))
        {
          $checkSub = $userUuid;

          if (!$this->isValidUuid_fsm($tokenData->sub))
          {
            $checkSub = $name;
          }

          if (strtolower($tokenData->sub) === strtolower($checkSub))
          {
            $result = !empty($tokenData->mcs);
          }
          else
          {
            $logger->error('Token user ' . $tokenData->sub . ' does not match ' . $checkSub);
          }
        }
        else
        {
          $result = !empty($tokenData->mcs);
        }
      }
    }

    return $result;
  }


  /**
   * @param string $uuid
   *
   * @return UserEntity|bool
   *
   * @throws DependencyException
   * @throws NotFoundException
   * @throws DbException
   */
  public function getUserEntityByUuid (string $uuid): bool|UserEntity
  {
    $user = new UserEntity($this->m_container);

    $uuid = str_replace('-', '', $uuid);

    $user->readEntity(['uuid' => $uuid]);

    if (!empty($user->id))
    {
      return $user;
    }

    return false;
  }


  /**
   * Checks for valid UUID-Format (including invalid UUIDs from FSM with missing Version 4)
   *
   * @param string $uuid
   * @return bool
   */
  private function isValidUuid_fsm(string $uuid): bool
  {
    return preg_match('/^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/', $uuid) === 1;
  }
}