<?php

namespace IMATHUZH\Qfq\Core\Typo3;

use TYPO3\CMS\Core\Crypto\Random;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Database\Connection;
use TYPO3\CMS\Core\Database\Query\Restriction\EndTimeRestriction;
use TYPO3\CMS\Core\Database\Query\Restriction\HiddenRestriction;
use TYPO3\CMS\Core\Database\Query\Restriction\StartTimeRestriction;
use TYPO3\CMS\Core\Utility\GeneralUtility;

class FrontendUserRepository {
    protected Connection $connection;

    protected $table = 'fe_users';

    /**
     * Returns an instance of this class.
     *
     * The only purpose of this function is to minimize the amount
     * of Typo3 related code in QFQ classes.
     *
     * @return self
     */
    public static function getInstance(): self {
        return GeneralUtility::makeInstance(self::class);
    }

    public function __construct(
        ConnectionPool $connectionPool
    ) {
        $this->connection = $connectionPool->getConnectionForTable($this->table);
    }

    /**
     * Searches for a user in fe_users matching the provided username.
     * If $pages is provided, it must be the comma-separated list of uid of pages
     * on which the user must be located. Otherwise, the page is not checked.
     *
     * @param string $username the username to be searched for
     * @param string $pages a comma-separated list of uids
     * @return array|null
     * @throws \Doctrine\DBAL\DBALException
     * @throws \Doctrine\DBAL\Driver\Exception
     */
    public function findByUsername(string $username, string $pages = ''): ?array {
        return $this->find(['username' => $username], $pages);
    }

    /**
     * Searches for a user in fe_users matching all the criteria from $condition.
     * If $pages is provided, it must be the comma-separated list of uid of pages
     * on which the user must be located. Otherwise, the page is not checked.
     *
     * @param array $condition an associated array of pairs (field => value)
     * @param string $pages a comma-separated list of uids
     * @return array|null
     * @throws \Doctrine\DBAL\DBALException
     * @throws \Doctrine\DBAL\Driver\Exception
     */
    public function find(array $condition, string $pages = ''): ?array {
        $query = $this->connection->createQueryBuilder();
        $query->getRestrictions()->removeByType(HiddenRestriction::class);
        $query->getRestrictions()->removeByType(StartTimeRestriction::class);
        $query->getRestrictions()->removeByType(EndTimeRestriction::class);
        $expr = $query->expr();
        $conditions = array_map(
            fn($field, $value) => $expr->eq($field, $query->quote($value)),
            array_keys($condition),
            array_values($condition)
        );
        if ($pages) {
            $conditions[] = $expr->in('pid', $query->quote($pages));
        }
        return $query->select('*')
            ->from($this->table)
            ->where(...$conditions)
            ->setMaxResults(1)
            ->executeQuery()
            ->fetchAssociative() ?: null;
    }

    /**
     * Inserts a new user to the fe_users tables.
     *
     * This method filters out silently properties with no matching table column.
     * No checks are made whether required properties are present except the following:
     * - `password` is assigned a random value unless already provided
     * - `crdate` and `tstamp` are the current timestamps
     *
     * @param array $properties properties of the user
     * @return array
     */
    public function insert(array $properties): array {
        $fields = array_intersect_key(
            $properties,
            $GLOBALS['TCA'][$this->table]['columns']
        );
        $fields['crdate'] = $GLOBALS['EXEC_TIME'];
        $fields['tstamp'] = $GLOBALS['EXEC_TIME'];
        if (!isset($fields['password'])) {
            $fields['password'] = GeneralUtility::makeInstance(Random::class)
                ->generateRandomHexString(32);
        }
        $this->connection->insert($this->table, $fields);
        return $this->find(['uid' => $this->connection->lastInsertId()]);
    }

    /**
     * Updates an existing fe_users record.
     *
     * This method filters out silently properties with no matching table column
     * as well as `uid`, `username` and `crdate`, whereas `tstamp` is updated to
     * the current timestamp. The returned array contains the list of updated
     * properties.
     *
     * @param array $properties properties of the user
     * @return array
     */
    public function update(int $uid, array $properties): array {
        $fields = array_intersect_key(
            $properties,
            array_flip(array_diff(
                array_keys($GLOBALS['TCA'][$this->table]['columns']),
                ['uid', 'username', 'crdate']
            ))
        );
        $fields['tstamp'] = $GLOBALS['EXEC_TIME'];
        $this->connection->update($this->table, $fields, ['uid' => $uid]);
        return $fields;
    }

    /**
     * Unlocks a disabled account
     *
     * @param array $account
     * @return void
     */
    public function unlock(array &$account): void {
        if ($this->connection->update(
            $this->table,
            [
                'disable' => 0,
                'tstamp' => $GLOBALS['EXEC_TIME']
            ],
            ['uid' => $account['uid']]
        )) {
            $account['disable'] = 0;
            $account['tstamp'] = $GLOBALS['EXEC_TIME'];
        }
    }

    /**
     * Activates an inactive account
     *
     * If the account is expired, then it is extended till the provided timestamp.
     *
     * @param array $account
     * @param int $until
     * @return void
     */
    public function activate(array &$account, int $until): void {
        $updated = [];
        if ($account['starttime'] > $GLOBALS['EXEC_TIME']) {
            $updated['starttime'] = $account['starttime'] = $GLOBALS['EXEC_TIME'];
        }
        if ($account['endtime'] > 0 && $account['endtime'] < $GLOBALS['EXEC_TIME']) {
            $updated['endtime'] = $account['endtime'] = $until;
        }
        if ($updated) {
            $updated['tstamp'] = $account['tstamp'] = $GLOBALS['EXEC_TIME'];
            $this->connection->update($this->table, $updated, ['uid' => $account['uid']]);
        }
    }

    /**
     * Returns the status of the requested account or null if none exists.
     *
     * If an account exists, then the returned table provides the following data:
     * + uid: the uid of the account (int)
     * + locked: the status of the `disable` field (bool)
     * + active: the result of inspecting fields `starttime` and `endtime` (bool)
     *
     * @param string $username
     * @param string $pages
     * @return array|null
     * @throws \Doctrine\DBAL\DBALException
     * @throws \Doctrine\DBAL\Driver\Exception
     */
    public function getStatus(string $username, string $pages = ''): ?array {
        $user = $this->findByUsername($username, $pages);
        if ($user && !$user['deleted']) {
            return $this->getStatusOfAccount($user);
        } else {
            return null;
        }
    }

    public function getStatusOfAccount(array $account): array {
        $startTime = $account['starttime'];
        $endTime = $account['endtime'];
        $expired = $endTime > 0 && $endTime < $GLOBALS['EXEC_TIME'];
        $inactive = $startTime > $GLOBALS['EXEC_TIME'];
        return [
            'uid' => $account['uid'],
            'locked' => $account['disable'] ?? false,
            'expired' => $expired,
            'inactive' => $inactive || $expired
        ];
    }
}
