<?php
/**
 * Created by PhpStorm.
 * User: crose
 * Date: 2/2/20
 * Time: 9:02 AM
 */

namespace IMATHUZH\Qfq\Core\Typo3;

use IMATHUZH\Qfq\Core\Exception\ErrorHandler;
use IMATHUZH\Qfq\Core\Exception\Thrower;
use IMATHUZH\Qfq\Core\Helper\Path;
use IMATHUZH\Qfq\Core\Helper\Support;
use IMATHUZH\Qfq\Core\Helper\HelperFile;
use IMATHUZH\Qfq\Core\Store\Config;
use TYPO3\CMS\Core\Core\SystemEnvironmentBuilder;
use TYPO3\CMS\Core\Configuration\ConfigurationManager;
use TYPO3\CMS\Core\Context\Context;
use TYPO3\CMS\Core\Routing\PageArguments;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\VersionNumberUtility;
use TYPO3\CMS\Frontend\Authentication\FrontendUserAuthentication;

/**
 * Class T3Handler
 * @package IMATHUZH\Qfq\Core\Typo3
 */
class T3Handler {

    /**
     * Returns the login status of the frontend user
     * @return bool
     */
    public static function isFeUserLoggedIn(): bool
    {
        try {
            return class_exists(GeneralUtility::class) &&
                   GeneralUtility::makeInstance(Context::class)->getPropertyFromAspect('frontend.user', 'isLoggedIn');
        } catch (\Exception $e) {
            return false;
        }
    }

    /**
     * Returns the current FrontendUserAuthentication object (TYPO3 v13+ request attribute).
     * Returns null when no request is active (CLI, early bootstrap, websocket worker).
     *
     * @return FrontendUserAuthentication|null
     */
    public static function getFrontendUserAuthentication(): ?FrontendUserAuthentication {
        $request = $GLOBALS['TYPO3_REQUEST'] ?? null;
        if (!$request) {
            return null;
        }
        $feUser = $request->getAttribute('frontend.user');
        return $feUser instanceof FrontendUserAuthentication ? $feUser : null;
    }

    /**
     * Returns the FE user data array or null when no user is available.
     *
     * @return array|null
     */
    public static function getFeUser(): ?array {
        return self::getFrontendUserAuthentication()?->user ?? null;
    }

    /**
     * Returns a single property of the FE user, or the given default when missing.
     *
     * @param string $key
     * @param mixed $default
     * @return mixed
     */
    public static function getFeUserProperty(string $key, mixed $default = ''): mixed {
        return self::getFeUser()[$key] ?? $default;
    }

    /**
     * Returns the BE user data array or null when no user is available.
     *
     * @return array|null
     */
    public static function getBeUser(): ?array {
        $beUser = $GLOBALS['BE_USER'] ?? null;
        return is_object($beUser) ? ($beUser->user ?? null) : null;
    }

    /**
     * Returns a single property of the BE user, or the given default when missing.
     *
     * @param string $key
     * @param mixed $default
     * @return mixed
     */
    public static function getBeUserProperty(string $key, mixed $default = ''): mixed {
        return self::getBeUser()[$key] ?? $default;
    }

    /**
     * Returns the PageArguments object for the current request (TYPO3 v10+).
     *
     * @return PageArguments|null
     */
    public static function getPageArguments(): ?PageArguments {
        $args = ($GLOBALS['TYPO3_REQUEST'] ?? null)?->getAttribute('routing');
        return $args instanceof PageArguments ? $args : null;
    }

    /**
     * Returns the current page id (TYPO3 v13+ via PageArguments).
     *
     * @return int
     */
    public static function getPageId(): int {
        return self::getPageArguments()?->getPageId() ?? 0;
    }

    /**
     * Returns the current page type (TYPO3 v13+ via PageArguments).
     *
     * @return int
     */
    public static function getPageType(): int {
        return (int)(self::getPageArguments()?->getPageType() ?? 0);
    }

    /**
     * Returns the current PageInformation object (TYPO3 v13+ request attribute),
     * or null when not inside a frontend request. Typed as object to stay
     * compatible with potential namespace shifts between minor versions.
     *
     * @return object|null
     */
    public static function getPageInformation(): ?object {
        $info = ($GLOBALS['TYPO3_REQUEST'] ?? null)?->getAttribute('frontend.page.information');
        return is_object($info) ? $info : null;
    }

    /**
     * Returns the current page record (TYPO3 v13+ via PageInformation).
     *
     * @return array
     */
    public static function getPageRecord(): array {
        $info = self::getPageInformation();
        if ($info && method_exists($info, 'getPageRecord')) {
            $record = $info->getPageRecord();
            if (is_array($record)) {
                return $record;
            }
        }
        return [];
    }

    /**
     * Call logoff of current FE User
     */
    public static function feLogOff() {

        // Restore T3 ErrorHandler. T3 throws exceptions - those should be handled by T3!
        ErrorHandler::suspend();

        self::getFrontendUserAuthentication()?->logoff();

        // Activate QFQ ErrorHandler again.
        ErrorHandler::resume();
    }


    /**
     * Convert a cleartext password to a hash. Respects if 'salted passwords' are enabled.
     * The password hash type is taken from QFQ config, Argon2i is used by default.
     *
     * @param string $newPassword
     * @return string
     * @throws \CodeException
     * @throws \UserFormException
     * @throws \UserReportException
     */
    public static function getHash($newPassword) {

        self::t3AutoloadIfNotRunning();

        // On ZI RedHat hosts, PHP module for argon2i might be missing. As a consequence, even the constants are not defined.
        if (!defined('PASSWORD_ARGON2I')) {
            define('PASSWORD_ARGON2I', 'argon2i');
        }
        $type = Config::get(SYSTEM_PASWORD_HASH_TYPE) ?? PASSWORD_BCRYPT;
        if ($type === PASSWORD_ARGON2I) {
            // Use Argon2i algorithm (with current default options of typo3 in year 2020)
            $saltedPassword = password_hash($newPassword, PASSWORD_ARGON2I, ['memory_cost' => 65536, 'time_cost' => 16, 'threads' => 1]);
        } elseif ($type === PASSWORD_BCRYPT) {
            $hash = base64_encode(hash('sha384', $newPassword, true));
            $saltedPassword = password_hash($hash, PASSWORD_BCRYPT, ['cost' => 12]);
        } else {
            Thrower::userFormException('Password hashing type not recognized.', 'The following password hashing type is not recognized: ' . $type);
        }
        return $saltedPassword;
    }

    /**
     * Check if the salted password corresponds to the password.
     *
     * @param string $saltedPassword
     * @param string $password
     * @return bool
     * @throws \UserFormException
     */
    public static function checkPassword($saltedPassword, $password) {
        return password_verify($password, $saltedPassword);
    }

    /**
     * Load Typo3 autoloader if Typo3 is not instantiated
     *
     * @throws \UserFormException
     */
    public static function t3AutoloadIfNotRunning() {

        if (self::isTypo3Loaded()) {
            return;
        }

        $classLoader = require Path::absoluteApp('vendor/autoload.php');

        try {
            // Documentation: https://docs.typo3.org/m/typo3/reference-coreapi/main/en-us/ApiOverview/Bootstrapping/Index.html
            SystemEnvironmentBuilder::run(0, SystemEnvironmentBuilder::REQUESTTYPE_FE);
        } catch (\Throwable $e) {
            Thrower::userFormException('Manual Typo3 bootstrap failed.');
        }
    }

    /**
     * Update in `typo3conf/LocalConfiguration.php` QFQ config.
     * a) key!==false: a single $key/$value pair (not used anymore)
     * b) key===false: array in $value
     *
     * @param mixed $value
     * @param mixed $key
     * @param string $baseUrl
     * @throws \UserFormException
     */
    public static function updateT3QfqConfig($value, $key = false, $baseUrl = '') {

        if (defined('PHPUNIT_QFQ') || defined('QFQ_API') || defined('QFQ_EXTERNAL')) {
            // a) There is no typo3conf/LocalConfiguration.php in phpunit.
            // b) During QFQ_API: we won't update LocalConfiguration.php. In T3 V8: broken path  for LocalConfiguration if update during API call: just skip this now.
            return;
        }


        // Restore T3 ErrorHandler. T3 throws exceptions - those should be handled by T3!
        ErrorHandler::suspend();

        self::t3AutoloadIfNotRunning();

        $configurationManager = GeneralUtility::makeInstance(ConfigurationManager::class);

        $configT3 = $configurationManager->getLocalConfiguration();
        $configExtQfq = $configT3['EXTENSIONS'][EXT_KEY] ?? [];

        if ($key === false) {
            // merge both arrays. $value overwrites $configExtQfq
            $tmpArr = array_merge($configExtQfq, $value);

            // Specific values are dynamically replaced: restore saved value to store again. Unit tests are an example for this reason.
            foreach ([SYSTEM_THROW_GENERAL_ERROR, SYSTEM_SHOW_DEBUG_INFO, SYSTEM_REPORT_MIN_PHP_VERSION, SYSTEM_BASE_URL, SYSTEM_DB_UPDATE] as $key) {
                $tmpArr[$key] = $configExtQfq[$key];
            }

            if ($baseUrl != '') {
                $tmpArr[SYSTEM_BASE_URL] = $baseUrl;
            }

            $localConfQfq = $configExtQfq;
            // Take care to only write those elements who originally defined in QFQ Extension LocalConfiguration.php.
            $configExtQfq = array();
            // Take only such arguments which exist in T3/QFQ config
            foreach ($localConfQfq as $key => $value) {
                $configExtQfq[$key] = ($tmpArr[$key] ?? '');
            }

        } else {
            // key given: set indiviudal key/value
            $configExtQfq[$key] = $value;
        }

        $configT3['EXTENSIONS'][EXT_KEY] = $configExtQfq;

        // In #18793/#16717 we seem to have concurrent write access to LocalConfigration.php, initiated by QFQ,  which destroys the config.
        // Create a local qfq lock. With this, we can detect if there is another QFQ thread who tries to write to it.
        $configT3qfqLock = Path::absoluteConf(CONFIG_T3) . '.qfq.lock';
        $fp = @fopen($configT3qfqLock, "x");
        if ($fp !== false) {
            // Lock acquired - Write new config to typo3conf/LocalConfiguration.php
            $configurationManager->writeLocalConfiguration($configT3);
            // Release lock
            fclose($fp);
        } else {
            // Lock couldn't be acquired (another QFQ thread is running) - Check if lock is expired:
            if (false !== ($fp = @fopen("$configT3qfqLock", "r"))) {

                $fstat = fstat($fp);
                fclose($fp);
                $ctime = $fstat['ctime'] ?? 0;
                // Expire: after 30 seconds
                if (time() - $ctime > 30) {
                    // Lock expired - try to remove it.
                    HelperFile::unlink($configT3qfqLock);
                }
            }
        }

        // Activate QFQ ErrorHandler again.
        ErrorHandler::resume();
    }

    /**
     * Note: Loads Typo3 if not running.
     *
     * @return string
     */
    public static function getTypo3Version() {
        self::t3AutoloadIfNotRunning();
        return VersionNumberUtility::getNumericTypo3Version();
    }

    /**
     * Kept as a stub returning true. QFQ supports TYPO3 v13+ only, so every
     * legacy call site will automatically take the "modern" branch.
     *
     * @return bool
     */
    public static function typo3VersionGreaterEqual9(): bool {
        return true;
    }

    /**
     * Kept as a stub returning true. See typo3VersionGreaterEqual9.
     *
     * @return bool
     */
    public static function typo3VersionGreaterEqual10(): bool {
        return true;
    }

    /**
     * Slugs have been mandatory since TYPO3 v10. Kept as a stub so the
     * numerous call sites across Form, Report and Link can stay untouched.
     *
     * @return bool
     */
    public static function useSlugsInsteadOfPageAlias(): bool {
        return true;
    }

    /**
     * Returns true if Typo3 classes are available.
     *
     * @return bool
     */
    public static function isTypo3Loaded() {
        return class_exists(GeneralUtility::class);
    }

    /**
     * Get typo3 database configuration.
     *
     * @param int $dbIndexData
     * @param int $dbIndexQfq
     * @param int $dbIndexT3
     * @return array
     * @throws \UserFormException
     */
    public static function getTypo3DbConfig(int $dbIndexData, int $dbIndexQfq, int &$dbIndexT3): array {
        $config = array();
        for ($i = 1; $i <= 3; $i++) {
            if ($dbIndexData !== $i && $dbIndexQfq !== $i) {
                $dbIndexT3 = $i;
                continue;
            }
        }

        self::t3AutoloadIfNotRunning();

        $configurationManager = GeneralUtility::makeInstance(ConfigurationManager::class);

        // Same as $GLOBALS['TYPO3_CONF_VARS']['EXT']['extConf']['qfq']
        if (isset($GLOBALS['TYPO3_CONF_VARS']) && !defined('PHPUNIT_QFQ')) {
            $configT3 = $configurationManager->getLocalConfiguration();
            $typo3DbCredentials = $configT3[TYPO3_CONF_DB][TYPO3_CONF_CONNECTIONS][TYPO3_CONF_DEFAULT];
            $config['DB_' . $dbIndexT3 . '_USER'] = $typo3DbCredentials[TYPO3_DB_USER];
            $config['DB_' . $dbIndexT3 . '_PASSWORD'] = $typo3DbCredentials[TYPO3_DB_PASSWORD];
            $config['DB_' . $dbIndexT3 . '_SERVER'] = $typo3DbCredentials[TYPO3_DB_HOST];
            $config['DB_' . $dbIndexT3 . '_NAME'] = $typo3DbCredentials[TYPO3_DB_NAME];
        }

        return $config;
    }
}