<?php
declare(strict_types=1);
/**
 * @package qfq
 * @author kputyr
 * @date: 19.04.2024
 */

namespace IMATHUZH\Qfq\Core\Typo3;

require_once(__DIR__ . '/../../../vendor/autoload.php');

use IMATHUZH\Qfq\Core\Store\Session;
use IMATHUZH\Qfq\Core\Store\Sip;
use IMATHUZH\Qfq\Core\Store\Store;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Log\LoggerAwareInterface;
use Psr\Log\LoggerAwareTrait;
use TYPO3\CMS\Core\Authentication\AbstractAuthenticationService;
use TYPO3\CMS\Core\Configuration\ExtensionConfiguration;
use TYPO3\CMS\Core\Http\ServerRequestFactory;
use TYPO3\CMS\Core\SingletonInterface;
use TYPO3\CMS\Core\Utility\GeneralUtility;

/**
 * This class allows to authenticate user by Qfq and synchronize it with Typo3.
 *
 * In order to authenticate a user by Qfq, create a sip variable feUser with
 * the username and request a page with parameter logintype=login. It a local
 * user with this username exists, it will be logged in.
 */
class QfqAuthenticationService extends AbstractAuthenticationService implements LoggerAwareInterface, SingletonInterface {
    use LoggerAwareTrait;

    const AUTH_USER_SIP = 'qfqauthuser';
    const AUTH_CONF_SIP = 'qfqauthconf';

    protected ?array $user = null;

    protected FrontendUserRepository $repository;

    protected array $configuration;

    public function __construct(ExtensionConfiguration $extensionConfiguration) {
        $this->configuration = $extensionConfiguration->get('qfq');
    }

    /**
     * Finds a user provided by QFQ to authenticate. Returns `null` if none is found
     * and an array of user properties otherwise, with a boolean 'qfq/auth' property
     * to indicate whether the user should be authenticated or rejected.
     *
     * @return array|null
     * @throws \CodeException
     * @throws \Doctrine\DBAL\DBALException
     * @throws \Doctrine\DBAL\Driver\Exception
     */
    public function getUser(): ?array {
        $query = $this->getRequest()->getQueryParams();
        $userSip = $query[self::AUTH_USER_SIP] ?? null;
        $confSip = $query[self::AUTH_CONF_SIP] ?? null;

        // This user is not managed by QFQ
        if (!$userSip) {
            return null;
        }

        // Read user properties and service configuration passed in the sip store
        $this->openSipStore();
        $this->user = $this->validateUser($this->loadSipContent($userSip));
        if (!$this->user) {
            return null;
        }
        $config = array_merge(
            $this->getDefaultConfig(),
            $confSip ? $this->loadSipContent($confSip) : []
        );
        $this->logger->debug("loaded configuration: " . \json_encode($config));

        $this->user = array_merge([
            // intval() evaluates the first number in a comma-separated list
            // ignoring the rest or returns 0 if the list is empty
            'pid' => intval($config['storage']),
            'usergroup' => $config['groups']
        ], $this->user);

        $username = $this->user['username'];
        $failureResult = ['qfq/auth' => false];

        // Find the user or create one otherwise if allowed to do so
        $repository = $this->getRepository();
        $t3user = $repository->findByUsername($username, $config['storage']);

        if ($t3user) {
            // An account exists - check if active and not disabled
            $status = $repository->getStatusOfAccount($t3user);
            $isLocked = $status['locked'];
            $isInactive = $status['inactive'] || $status['expired'];

            $this->logger->debug("checking $username " . \json_encode($status));

            if ($isLocked) {
                if ($config['unlock']) {
                    $this->logger->info("unlocking a disabled account $username");
                    $repository->unlock($t3user);
                } else {
                    $this->logger->warning("failure: the account $username is locked");
                    $this->updateAuthResult(AUTH_STATUS_LOCKED);
                    return $failureResult;
                }
            }
            if ($isInactive) {
                if ($config['activate']) {
                    $this->logger->info("activating an inactive account $username");
                    $extendUntil = intval($config['extendUntil'])
                        ?: $GLOBALS['EXEC_TIME'] + intval($config['extendBy']);
                    $repository->activate($t3user, $extendUntil);
                } else {
                    $this->logger->warning("failure: the account $username is inactive");
                    $this->updateAuthResult($status['expired'] ? AUTH_STATUS_EXPIRED : AUTH_STATUS_INACTIVE);
                    return $failureResult;
                }
            }

            // Update the account if configured so and some user parameters have been passed
            // count($this->user) == 1 implies that the table contains only the username
            if ($config['update'] && count($this->user) > 1) {
                $this->logger->info("updating the account $username");
                $t3user = array_merge($t3user, $repository->update($t3user['uid'], $this->user));
            }

            $t3user['qfq/auth'] = true;
            $this->updateAuthResult(AUTH_STATUS_OK);

        } elseif ($config['create']) {
            // No account exists, but it can be created
            $this->logger->info("creating an account for $username");
            $t3user = $repository->insert($this->user);
            $t3user['qfq/auth'] = true;
            $this->updateAuthResult(AUTH_STATUS_OK);

        } else {
            // No account exists and one cannot be created
            $this->logger->warning("failure: no account $username exists");
            $this->updateAuthResult(AUTH_STATUS_NOT_FOUND);
            return $failureResult;
        }

        return $t3user;
    }

    protected function updateAuthResult(string $value) {
        Store::getInstance()->setVar(VAR_AUTH_RESULT, $value, STORE_VAR, true);
    }

    /**
     * Authenticates the user if it managed by this service.
     *
     * @param array $user
     * @return int
     */
    public function authUser(array $user): int {
        if (!isset($user['qfq/auth'])) {
            return 100;     // another service should authenticate the user
        } elseif ($user['qfq/auth']) {
            return 200;     // the user is authenticated
        } else {
            return 0;       // the user is locked or cannot be created
        }
    }

    /**
     * The default configuration of the service taken from the settings of the extension.
     *
     * @return array
     */
    protected function getDefaultConfig(): array {
        return [
            'storage' => $this->configuration['authUserStorage'],
            'groups' => $this->configuration['authUserGroups'],
            'update' => (bool)$this->configuration['authUpdateAccount'],
            'create' => (bool)$this->configuration['authCreateAccount'],
            'unlock' => (bool)$this->configuration['authUnlockAccount'],
            'activate' => (bool)$this->configuration['authActivateAccount'],
            'extendBy' => (int)$this->configuration['authExtendByDays'] * 86400,
            // Ensure that 'extendUntil' is available. If not overrider,
            // 'extendBy' will be used
            'extendUntil' => 0
        ];
    }

    protected function getRepository(): FrontendUserRepository {
        if (!isset($this->repository)) {
            $this->repository = GeneralUtility::makeInstance(FrontendUserRepository::class);
        }
        return $this->repository;
    }

    /**
     * Replaces the user with `null` if it does not contain a username.
     *
     * @param array|null $user
     * @return array|null
     */
    protected function validateUser(?array $user): ?array {
        return empty($user) || empty($user['username']) ? null : $user;
    }

    /**
     * The current request
     *
     * @return ServerRequestInterface
     */
    protected function getRequest(): ServerRequestInterface {
        return $GLOBALS['TYPO3_REQUEST'] ?? ServerRequestFactory::fromGlobals();
    }

    /**
     * Initiates the session in order to access sip values
     * @return void
     * @throws \CodeException
     */
    protected function openSipStore(): void {
        // This initiates the PHP session
        Session::getInstance();
    }

    /**
     * Returns the content of a given sip omitting empty values.
     *
     * @param string $sip
     * @return array
     */
    protected function loadSipContent(string $sip): array {
        parse_str($_SESSION['qfq'][$sip] ?? '', $result);
        $this->logger->debug("sip content: " . \json_encode($result, JSON_PRETTY_PRINT));
        return array_filter($result, fn($value) => $value != '');
    }

    /**
     * Adds query parameters that are necessary to trigger the authentication process in Typo3
     * and prepares sip stores with authentication data.
     *
     * @param Sip $sip
     * @param string $username
     * @param string $userData
     * @param string $authFeature
     * @return array
     * @throws \CodeException
     * @throws \UserFormException
     */
    public static function generateUrlParameters(Sip $sip, string $username, string $userData, string $authFeature): array {
        $userData .= ($userData ? '&' : '') . "username=$username";
        return [
            'logintype' => 'login',
            self::AUTH_USER_SIP => $sip->queryStringToSip($userData, RETURN_SIP),
            self::AUTH_CONF_SIP => $sip->queryStringToSip($authFeature, RETURN_SIP)
        ];
    }


}
