<?php
/**
 * app/models/sessions.php
 *
 * This model is used for the modification and management of the session data.
 *
 * Notes: After refactor, the sessions will use ID's for short term, and Cookies
 * will use the token for long term storage
 *
 * @version 3.0
 * @author  Joey Kimsey <Joey@thetempusproject.com>
 * @link    https://TheTempusProject.com
 * @license https://opensource.org/licenses/MIT [MIT LICENSE]
 */
namespace TheTempusProject\Models;

use TheTempusProject\Bedrock\Functions\Check;
use TheTempusProject\Bedrock\Functions\Code;
use TheTempusProject\Canary\Bin\Canary as Debug;
use TheTempusProject\Bedrock\Functions\Session;
use TheTempusProject\Bedrock\Functions\Cookie;
use TheTempusProject\Classes\DatabaseModel;
use TheTempusProject\Classes\Config;
use TheTempusProject\TheTempusProject as App;

class Sessions extends DatabaseModel {
    public $tableName = 'sessions';
    public $modelVersion = '1.0';
    public $databaseMatrix = [
        [ 'userID', 'int', '5' ],
        [ 'userGroup', 'int', '5' ],
        [ 'expire', 'int', '10' ],
        [ 'ip', 'varchar', '15' ],
        [ 'hash', 'varchar', '80' ],
        [ 'lastPage', 'varchar', '64' ],
        [ 'username', 'varchar', '20' ],
        [ 'token', 'varchar', '120' ],
    ];
    public static $activeSession = false;

    /**
     * The model constructor.
     */
    public function __construct() {
        parent::__construct();
        self::$session = $this;
    }

    /**
     * Check if a session exists, verifies the username,
     * password, and IP address to prevent forgeries.
     *
     * @param {int} [$id] - The id of the session being checked.
     * @return {bool}
     */
    public function checkSession( $sessionID ) {
        $user = new User;
        // @todo lets put this on some sort of realistic checking regime other than check everything every time
        if ( $sessionID == false ) {
            Debug::log( 'sessionID false' );
            return false;
        }
        if ( !Check::id( $sessionID ) ) {
            Debug::log( 'sessionID not id' );
            return false;
        }
        $data = self::$db->get( $this->tableName, [ 'ID', '=', $sessionID ] );
        if ( $data->count() == 0 ) {
            Debug::info( 'Session token not found.' );
            return false;
        }
        $session = $data->first();
        $user = $user->findById( $session->userID );
        if ( $user === false ) {
            Debug::info( 'User not found in DB.' );
            $this->destroy( $session->ID );
            return false;
        }
        if ( $user->username != $session->username ) {
            Debug::info( 'Usernames do not match.' );
            $this->destroy( $session->ID );
            return false;
        }
        if ( $user->password != $session->hash ) {
            Debug::info( 'Session Password does not match.' );
            $this->destroy( $session->ID );
            return false;
        }
        if ( time() > $session->expire ) {
            Debug::info( 'Session Expired.' );
            $this->destroy( $session->ID );
            return false;
        }
        if ( $user->userGroup !== $session->userGroup ) {
            Debug::info( 'Groups do not match.' );
            $this->destroy( $session->ID );
            return false;
        }
        if ( $_SERVER['REMOTE_ADDR'] != $session->ip ) {
            Debug::info( 'IP addresses do not match.' );
            $this->destroy( $session->ID );
            return false;
        }
        self::$activeSession = $session;
        return true;
    }

    /**
     * Checks the "remember me" cookie we use to identify
     * unique sessions across multiple visits. Checks that
     * the tokens match, checks the username as well as the
     * password from the database to ensure it hasn't been
     * modified elsewhere between visits.
     *
     * @param {string} [$token] - The unique token saved as a cookie that is being checked.
     * @return {bool}
     */
    public function checkCookie( $cookieToken, $create = false ) {
        $user = new User;
        if ( $cookieToken === false ) {
            Debug::info( 'cookieToken false' );
            return false;
        }
        $data = self::$db->get( $this->tableName, [ 'token', '=', $cookieToken ] );
        if ( !$data->count() ) {
            Debug::info( 'sessions->checkCookie - Session token not found.' );
            return false;
        }
        $session = $data->first();
        $user = self::$user->findById( $session->userID );
        if ( $user === false ) {
            Debug::info( 'sessions->checkCookie - could not find user by ID.' );
            return false;
        }
        if ( $user->username != $session->username ) {
            Debug::info( 'sessions->checkCookie - Usernames do not match.' );
            $this->destroy( $session->ID );
            return false;
        }
        if ( $user->password != $session->hash ) {
            Debug::info( 'sessions->checkCookie - Session Password does not match.' );
            $this->destroy( $session->ID );
            return false;
        }
        if ( $create ) {
            return $this->newSession( null, false, false, $session->userID );
        }
        return true;
    }

    /**
     * Creates a new session from the data provided. The
     * expiration time is optional and will be set to the
     * system default if not provided.
     *
     * @param {int} [$ID] - The User ID of the new session holder.
     * @param {int} [$expire] - The expiration time (in seconds).
     * @return {bool}
     */
    public function newSession( $expire = null, $override = false, $remember = false, $userID = null ) {
        if ( empty( $expire ) ) {
            // default Session Expiration is 24 hours
            $expireLimit = Config::getValue( 'main/loginTimer' );
            $expire = ( time() + $expireLimit );
            Debug::log( 'Using default expiration time' );
        }
        $lastPage = App::getUrl();
        if ( self::$activeSession != false ) {
            // there is already an active session
            if ( $override === false ) {
                Debug::error( 'No need for a new session.' );
                return false;
            }
            // We can override the active session
            $data = self::$db->get( $this->tableName, [ 'userID', '=', self::$activeSession->ID ] );
            if ( $data->count() ) {
                Debug::log( 'Deleting old session from db' );
                $session = self::$db->first();
                $this->destroy( $session->ID );
            }
        }
        if ( empty( $userID ) ) {
            if ( App::$activeUser === null ) {
                Debug::info( 'Must provide user details to create a new session.' );
                return false;
            }
            $userID = App::$activeUser->ID;
        }
        $userObject = self::$user->findById( $userID );
        if ( $userObject === false ) {
            Debug::info( 'User not found.' );
            return false;
        }
        $token = Code::genToken();
        $result = self::$db->insert(
            $this->tableName,
            [
                'username' => $userObject->username,
                'hash' => $userObject->password,
                'userGroup' => $userObject->userGroup,
                'userID' => $userObject->ID,
                'lastPage' => $lastPage,
                'expire' => $expire,
                'ip' => $_SERVER['REMOTE_ADDR'],
                'token' => $token,
            ]
        );
        $sessionID = self::$db->lastId();


        $sessionData = self::$db->get( $this->tableName, [ 'ID', '=', $sessionID ] )->first();


        Session::put( 'SessionID', $sessionID );
        if ( $remember ) {
            Cookie::put( 'RememberToken', $token, ( time() + ( 3600 * 24 * 30 ) ) );
        }
        self::$activeSession = $sessionData;
        return true;
    }

    /**
     * Function to update the users' current active page.
     * NOTE: Current session assumed if no $id is provided.
     *
     * @param {string} [$page] - The name of the page you are updating to.
     * @param {int|null} [$id] - The ID of the session you are updating.
     * @return {bool}
     */
    public function updatePage( $page, $id = null ) {
        if ( empty( $id ) ) {
            if ( self::$activeSession === false ) {
                Debug::info( 'Session::updatePage - Must provide session ID or have active session' );
                return false;
            }
            $id = self::$activeSession->ID;
        }
        if ( !Check::id( $id ) ) {
            Debug::info( 'Session::updatePage - Invalid ID' );
            return false;
        }
        if ( !self::$db->update( $this->tableName, $id, [ 'lastPage' => $page ] ) ) {
            Debug::info( 'Session::updatePage - Failed to update database' );
            return false;
        }
        return true;
    }

    /**
     * Destroy a session.
     *
     * @param {int} [$id] - The ID of the session you wish to destroy.
     * @return {bool}
     */
    public function destroy( $id ) {
        Session::delete( 'SessionID' );
        Cookie::delete( 'RememberToken' );
        if ( !Check::id( $id ) ) {
            Debug::info( 'Session::destroy - Invalid ID' );
            return false;
        }
        $data = self::$db->get( $this->tableName, [ 'ID', '=', $id ] );
        if ( !$data->count() ) {
            Debug::info( 'Session::destroy - Session not found in DB' );
            return false;
        }
        self::$db->delete( $this->tableName, [ 'ID', '=', $id ] );
        self::$activeSession = false;
        return true;
    }
}
