<?php
/**
 * Created by PhpStorm.
 * User: crose
 * Date: 5/4/16
 * Time: 1:34 PM
 */

namespace IMATHUZH\Qfq\Core\Store;


use IMATHUZH\Qfq\Core\Typo3\T3Handler;


/**
 * Class Session
 * @package qfq
 */
class Session {

    private static $instance = null;
    private static $phpUnit = null;
    private static $sessionLocal = array();
    private static $sessionId = null;
    private static $sessionOpen = false;
    private static $lastActivity = false;
    private static $flagChangedCookieFe = false;

    /**
     * @param bool|false $phpUnit
     *
     * @throws \CodeException
     */
    private function __construct($phpUnit = false) {

        #TODO: rewrite $phpUnit to: "if (!defined('PHPUNIT_QFQ')) {...}"
        if (self::$phpUnit !== null) {
            throw new \CodeException("Try to set flag phpunit again - that should not happen.", ERROR_CODE_SHOULD_NOT_HAPPEN);
        }

        self::$phpUnit = $phpUnit;

        if (self::$phpUnit === true) {
            self::$sessionLocal = array();
        } else {

            ini_set('session.cookie_httponly', 1);

            $lifetime = SYSTEM_COOKIE_LIFETIME;

            $path = $this->getSitePath();

            $secure = false; // if you only want to receive the cookie over HTTPS
            $httponly = true; // prevent JavaScript access to session cookie
            $samesite = 'lax';

            // Needed expire date for header() method
            $expireDate = date("D, d-M-Y H:i:s", strtotime('+2 days')) . ' GMT';

            // More information about previous dots in domains: https://stackoverflow.com/questions/348282/php-cookie-domain-subdomain-control
            if (PHP_VERSION_ID < 70300) {
                session_set_cookie_params($lifetime, $path . ';SameSite=' . $samesite, null, $secure, $httponly);
            } else {
                session_set_cookie_params([
                    'lifetime' => $lifetime,
                    'path' => $path,
                    'domain' => null,
                    'secure' => $secure,
                    'httponly' => $httponly,
                    'samesite' => $samesite
                ]);
            }

            // Old way to set cookie_params without samesite - samesite is needed later
            // session_set_cookie_params($lifetime, $path);

            $currentCookieParams = session_get_cookie_params();
            session_name(SESSION_NAME);

            session_start();

            // Currently, setcookie() is only called to really extend the lifetime. All other parameter needs to be given again.
            // For PHP Version < 7.3 cookie with samesite can only be set over the header() method.
            // Secure is actually set to false because of testing instance, otherwise SIP cant get requested without HTTPS.
            // Setup for Secure-> true : 1. Change value of $secure from false to true. 2. Write String "Secure;" after "HttpOnly;" in header() function below.
            if (PHP_VERSION_ID < 70300) {
// header() here makes no senss, cause it's to late: T3 already sent some non header content.
//                header("Set-Cookie: ".SESSION_NAME."=".session_id()."; expires=".$date."; path=".$path."; domain=".$currentCookieParams['domain']."; HttpOnly; SameSite=Lax");
            } else {
                setcookie(SESSION_NAME, session_id(), [
                    'expires' => time() + $lifetime,
                    'path' => $path,
                    'httponly' => $httponly,
                    'secure' => $secure,
                    'samesite' => 'Lax'
                ]);
            }

            // Old way to set cookie without samesite - samesite is needed later
            //  setcookie(SESSION_NAME, session_id(), time() + $lifetime, $path, $currentCookieParams['domain'], $currentCookieParams['secure'], true);

            self::$sessionId = session_id();
        }

        self::$sessionOpen = true;

        self::checkFeUserUid();

    }

    /**
     * Extract the SitePath of the current T3 installation.
     *
     * @return bool|string - <path>  with a trailing '/'
     * @throws \CodeException
     */
    private static function getSitePath() {

        if (empty($_SERVER['SCRIPT_NAME'])) {
            throw new \CodeException('Missing _SERVER[SCRIPT_NAME]', ERROR_SESSION_BROKEN_SCRIPT_PATH);
        }

        $path = $_SERVER['SCRIPT_NAME'] ?? '';
        $pos = strrpos($path, '/');

        if ($pos === false) {
            throw new \CodeException("Broken _SERVER[SCRIPT_NAME]: $path", ERROR_SESSION_BROKEN_SCRIPT_PATH);
        }
        // Remove PHP script
        $path = substr($path, 0, $pos + 1);

        // QFQ might be called by API - justify to the SitePath
        $pos = strpos($path, 'typo3conf/');
        if ($pos !== false) {
            $path = substr($path, 0, $pos - strlen($path));
        }

        if (empty($path)) {
            throw new \CodeException("Broken _SERVER[SCRIPT_NAME]: $path", ERROR_SESSION_BROKEN_SCRIPT_PATH);
        }

        return $path;
    }

    /**
     * Free a lock on the current session
     */
    public static function close() {

        if (self::$sessionOpen) {
            session_write_close();
        }
        self::$sessionOpen = false;

    }

    /**
     * Destroy a session - this is only needed in case of attacks
     */
    public static function destroy() {

        if (!defined('PHPUNIT_QFQ')) {

            if (isset($_COOKIE[SESSION_NAME])) {
                unset($_COOKIE[SESSION_NAME]);
                setcookie(SESSION_NAME, '', time() - 86400, '/'); // empty value and old timestamp
            }

            session_destroy();
        }

        $_SESSION = array();
    }

    /**
     * By default the session is opened during the bootstrap. In case the session has been closed manually, reopen it here.
     * This code has never been tested - it should not happen that a session needs manually closed and reopen than.
     * Nevertheless: This code is therefore a fallback in case the reopen really happens somewhat. Maybe this happens in
     * upcoming logic.
     *
     */
    public static function open() {

        if (self::$sessionOpen != true && self::$sessionId != null) {
            session_id(self::$sessionId);
            session_start();
            self::$sessionOpen = true;
        }
    }

    /**
     * Check if the feUserUid is stored in the session (even with 'false' which indicates not logged in user).
     *   If not,
     *    - clear the session
     *    - save the feUser, feUserUid in the session.
     *
     * Check if the recent logged in feUserUid is equal to the one stored in session: If different, invalidate (clear)
     * the session and save the new feUser, feUserUid in the session. If isset($GLOBALS["TSFE"]), than we're in a T3
     * environment, else we are called as API classes and need to fake feUser / feUserUid from previous stored session.
     * It's necessary to have feUser / feUserUid available in API classes, due to dynamic update which might reload
     * data based on feUser / feUserUid.
     */
    private static function checkFeUserUid() {

        $feUserUidSession = Session::get(SESSION_FE_USER_UID);
        $feUserSession = Session::get(SESSION_FE_USER);
        $feUserGroup = false;

        // Session Timeout only exists for logged in FE users - the default is no user logged in, so set to false to switch of session expiration.
        self::$lastActivity = false;

        if (isset($GLOBALS["TSFE"])) {
            // if no one is logged in: 0
            $feUidLoggedIn = $GLOBALS["TSFE"]->fe_user->user["uid"] ?? false;
            $feUserSession = $GLOBALS["TSFE"]->fe_user->user["username"] ?? false;
            $feUserGroup = $GLOBALS["TSFE"]->fe_user->user["usergroup"] ?? false;
            $beUser = $GLOBALS["BE_USER"]->user["username"] ?? false;
            $languageId = T3Info::getLanguageId() ?? false;
            $languagePath = T3Info::getLanguagePath($languageId) ?? false;
            $pageId = $GLOBALS["TSFE"]->id ?? 0;

            // Cookie identifier
            $cookieFe = ($_COOKIE['fe_typo_user']) ?? false;
            if ($cookieFe !== self::get(SESSION_LAST_COOKIE_FE)) {
                self::$flagChangedCookieFe = true; // Set the flag that the FE_USER User has changed
                // Update SESSION_LAST_FE_COOKIE
                self::set(SESSION_LAST_COOKIE_FE, $cookieFe);
            }

            // Manage Custom Session Timeout for logged in users
            if (isset($GLOBALS["TSFE"]->fe_user->user["username"]) && isset($_COOKIE['fe_typo_user'])) {

                if (self::$flagChangedCookieFe) {
                    // New user: start timeout timer
                    self::$lastActivity = time();
                } else {
                    // ok, still the same user is logged in: get the last activity timestamp to compare later against timeout.
                    self::$lastActivity = self::get(SESSION_LAST_ACTIVITY);
                }
            }

            // page language should be saved in session even fe user is not logged in.
            Session::set(SESSION_PAGE_LANGUAGE, $languageId);
            Session::set(SESSION_PAGE_LANGUAGE_PATH, $languagePath);
            Session::set(SESSION_PAGE_ID, $pageId);
        } else {
            // If we are called through API there is no T3 environment. Assume nothing has changed, and fake the following check to always 'no change'.
            $feUidLoggedIn = $feUserUidSession;
        }

        if ($feUidLoggedIn != $feUserUidSession) {

            // save new feUserUid, feUserName
            Session::set(SESSION_FE_USER_UID, $feUidLoggedIn);
            Session::set(SESSION_FE_USER, $feUserSession);
            Session::set(SESSION_FE_USER_GROUP, $feUserGroup);
            Session::set(SESSION_BE_USER, $beUser);
        }

    }

    /**
     * Return content to given $key (=SIP).
     * Return 'false' if not found.
     *
     * @param $key
     *
     * @return bool
     */
    public static function get($key) {

        if (!self::$sessionOpen) {
            self::open();
        }

        if (self::$phpUnit) {
            $value = isset(self::$sessionLocal[$key]) ? self::$sessionLocal[$key] : false;
        } else {
            $value = isset($_SESSION[SESSION_NAME][$key]) ? $_SESSION[SESSION_NAME][$key] : false;
        }

        return $value;
    }

    /**
     *
     */
    public static function clearAll() {

        if (!self::$sessionOpen) {
            self::open();
        }

        if (self::$phpUnit) {
            self::$sessionLocal = array();
        } else {
            $_SESSION[SESSION_NAME] = array();
        }
    }

    /**
     * @param $key
     * @param $value
     */
    public static function set($key, $value) {

        if (!self::$sessionOpen) {
            self::open();
        }

        if (self::$phpUnit) {
            self::$sessionLocal[$key] = $value;
        } else {
            $_SESSION[SESSION_NAME][$key] = $value;
        }
    }

    /**
     * Unset the given $key
     *
     * @param $key
     */
    public static function unsetItem($key) {

        if (!self::$sessionOpen) {
            self::open();
        }

        if (isset($_SESSION[SESSION_NAME][$key])) {
            unset($_SESSION[SESSION_NAME][$key]);
        }

    }

    /**
     * @param bool|false $phpUnit
     *
     * @return Session class
     * @throws \CodeException
     */
    public static function getInstance($phpUnit = false) {

        // Design Pattern: Singleton
        if (self::$instance === null) {
            self::$instance = new self($phpUnit);
        }

        if (!self::$sessionOpen) {
            self::open();
        }

        return self::$instance;
    }

    /**
     * Checks if the QFQ session is expired.
     *
     * @param $timeout
     * @throws \UserFormException
     */
    public static function checkSessionExpired($timeout) {

        // Just to be sure that the given $timeout is supported by the current php.ini setup
        self::checkSessionTimeout($timeout);

        if (self::$lastActivity === false || $timeout === false || $timeout == 0) {
            return;
        }

        if (time() - self::$lastActivity > $timeout) {

            T3Handler::feLogOff();
            self::destroy();
        }
    }

    /**
     * Check Session Timeout
     *
     * @param $timeout
     * @throws \UserFormException
     */
    public static function checkSessionTimeout($timeout) {

        if (self::getPhpSessionTimeout() < $timeout) {
            throw new \UserFormException ("The specified timeout of $timeout seconds is higher than the PHP config 'session.gc_maxlifetime' ("
                . ini_get('session.gc_maxlifetime') . ") and/or 'session.cookie_lifetime' ("
                . ini_get('session.cookie_lifetime') . ")");
        }

        if ($timeout > SYSTEM_COOKIE_LIFETIME) {
            throw new \UserFormException ("The specified timeout of $timeout seconds is higher than the hardcoded cookie lifetime (" . SYSTEM_COOKIE_LIFETIME . ")");
        }
    }

    /**
     * Get the minimum of 'session.cookie_lifetime' and 'session.gc_maxlifetime'
     * A zero means unlimited and will be limited to one day.
     *
     * @return int|string
     */
    public static function getPhpSessionTimeout() {
        $timeout = ini_get('session.cookie_lifetime');
        $gc_maxlifetime = ini_get('session.gc_maxlifetime');

        if ($timeout == 0) {
            $timeout = 86400;
        }

        if ($gc_maxlifetime == 0) {
            // Just to set a limit
            $timeout = 86400;
        }
        // gc_maxlifetime should be ignored when the value is '' / false #20505
        if ($gc_maxlifetime === '' || $gc_maxlifetime === false) {
            return $timeout;
        }

        return $timeout > $gc_maxlifetime ? $gc_maxlifetime : $timeout;
    }

    /**
     * Returns $flagFeUserChanged. In case it's true, set it to false.
     *
     * @return bool
     */
    public static function getAndDestroyFlagFeUserHasChanged() {

        $flag = self::$flagChangedCookieFe;

        self::$flagChangedCookieFe = false;

        return $flag;
    }

    /**
     * Stores SQL performance data into the session.
     *
     * Adds or updates the execution time for a specific SQL query key in the session-based
     * performance tracking array. Useful for collecting and later visualizing query performance
     * in the Dev Panel.
     *
     * @param string $level
     * @param string $query
     * @param int $time Execution time of the query in milliseconds.
     * @return void
     */
    public static function fillSqlPerformance(string $level, string $query, int $time): void {
        $performanceArray = self::get(PERFORMANCE_SESSION_KEY) ?? [];

        // Ensure there's at least one content block
        if (empty($performanceArray)) {
            return;
        }

        // Get the current content / Last
        $lastIndex = array_key_last($performanceArray);

        // Initialize level if not present
        if (!isset($performanceArray[$lastIndex][$level])) {
            $performanceArray[$lastIndex][$level] = [
                DEV_PANEL_PERFORMANCE_ARRAY_KEY_QUERY => $query,
                DEV_PANEL_PERFORMANCE_ARRAY_KEY_TIME => $time
            ];
        } else {
            // If it exists, just update the time
            $performanceArray[$lastIndex][$level][DEV_PANEL_PERFORMANCE_ARRAY_KEY_TIME] += $time;
        }

        // Save updated session data
        self::set(PERFORMANCE_SESSION_KEY, $performanceArray);
    }

    /**
     * Adds an empty Subarray for the Dev Panel.
     * Used to make distinctions between different Reports.
     * @return void
     */
    public static function addEmptyContentBlock(): void {
        $performanceArray = self::get(PERFORMANCE_SESSION_KEY) ?? [];
        $performanceArray[] = [];
        self::set(PERFORMANCE_SESSION_KEY, $performanceArray);
    }
}