<?php
/**
 * app/models/user.php
 *
 * This class is used for the manipulation of the user database table.
 *
 * @todo needs a re-build
 * @todo finish fixing the check functions that were migrated here
 *              These could go in the Forms class?
 *
 * @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\Canary\Bin\Canary as Debug;
use TheTempusProject\Bedrock\Functions\Hash;
use TheTempusProject\Bedrock\Functions\Session;
use TheTempusProject\Bedrock\Functions\Code;
use TheTempusProject\Bedrock\Classes\Config;
use TheTempusProject\Canary\Classes\CustomException;
use TheTempusProject\Classes\Email;
use TheTempusProject\Classes\DatabaseModel;
use TheTempusProject\Classes\Preferences;
use TheTempusProject\Classes\Forms;
use TheTempusProject\TheTempusProject as App;

class User extends DatabaseModel {
    public $tableName = 'users';
    public $modelVersion = '1.0';
    public $databaseMatrix = [
        [ 'registered', 'int', '10' ],
        [ 'terms', 'int', '1' ],
        [ 'confirmed', 'int', '1' ],
        [ 'userGroup', 'int', '11' ],
        [ 'lastLogin', 'int', '10' ],
        [ 'username', 'varchar', '16' ],
        [ 'password', 'varchar', '80' ],
        [ 'email', 'varchar', '75' ],
        [ 'name', 'varchar', '20' ],
        [ 'confirmationCode', 'varchar', '80' ],
        [ 'prefs', 'text', '' ],
    ];
    public $permissionMatrix = [
        'uploadImages' => [
            'pretty' => 'Upload images (such as avatars)',
            'default' => false,
        ],
    ];
    public $preferenceMatrix = [
        'gender' => [
            'pretty' => 'Gender',
            'type' => 'select',
            'default' => 'unspecified',
            'options' => [
                'male',
                'female',
                'other',
                'unspecified',
            ],
        ],
        'newsletter' => [
            'pretty' => 'Receive our Newsletter?',
            'type' => 'checkbox',
            'default' => 'true',
        ],
        'avatar' => [
            'pretty' => 'Avatar',
            'type' => 'file',
            'default' => 'images/defaultAvatar.png',
        ],
        'timezone' => [
            'pretty' => 'Timezone',
            'type' => 'timezone',
            'default' => 'America/New_York',
        ],
        'dateFormat' => [
            'pretty' => 'Date Format',
            'type' => 'select',
            'default' => 'F j, Y',
            'options' => [
                '1-8-1991' => 'n-j-Y',
                '8-1-1991' => 'j-n-Y',
                '01-08-1991' => 'm-d-Y',
                '08-01-1991' => 'd-m-Y',
                'January 8, 1991' => 'F-j-Y',
                '8 January, 1991' => 'j-F-Y',
                'January 08, 1991' => 'F-d-Y',
                '08 January, 1991' => 'd-F-Y',
                'Jan 8, 1991' => 'M-j-Y',
                '8 Jan 1991' => 'j-M-Y',
                'Jan 08, 1991' => 'M-d-Y',
                '08 Jan 1991' => 'd-M-Y',
            ],
        ],
        'timeFormat' => [
            'pretty' => 'Time Format',
            'type' => 'select',
            'default' => 'g:i:s A',
            'options' => [
                '3:33:33 AM' => 'g:i:s A',
                '03:33:33 AM' => 'h:i:s A',
                '3:33:33 am' => 'g:i:s a',
                '03:33:33 am' => 'h:i:s a',
                '3:33:33 (military)' => 'G:i:s',
                '03:33:33 (military)' => 'H:i:s',
            ],
        ],
        'pageLimit' => [
            'pretty' => 'Items Displayed Per Page',
            'type' => 'select',
            'default' => '10',
            'options' => [
                '10',
                '15',
                '20',
                '25',
                '50',
            ],
        ],
        'darkMode' => [
            'pretty' => 'Enable Dark-Mode viewing',
            'type' => 'checkbox',
            'default' => 'false',
        ],
    ];
    protected static $avatars;
    protected static $preferences;
    protected static $group;
    protected static $usernames;
    protected $data;

    public function __construct() {
        parent::__construct();
        self::$user = $this;
        self::$preferences = new Preferences;
        self::$group = new Group;
    }

    public function getPreferences( $id ) {
        if ( !Check::id( $id ) ) {
            return false;
        }
        $userData = $this->get( $id );
        $prefs = json_decode( $userData->prefs, true );
        return $prefs;
    }

    public function getPreferencesDelta() {
        $defaults = $this->getDefaultPreferences();
        foreach ( $defaults as $key => $value ) {
            if ( isset( self::$preferences[ $key ] ) ) {
                $defaults[ $key ] = self::$preferences[ $key ];
            }
        }
        return $defaults;
    }

    public function getDefaultPreferences() {
        return self::$preferences->getDefaultPreferencesArray();
    }

    /**
     * Check the database for a user with the same email.
     *
     * @param {string} [$email] - The email being tested.
     * @return {bool}
     */
    public function noEmailExists( $email ) {
        if ( Check::email( $email ) ) {
            $emailQuery = self::$db->get( $this->tableName, [ 'email', '=', $email ] );
            if ( $emailQuery->count() == 0 ) {
                return true;
            }
        }
        // self::addError("Email is already in use.", $email);
        return false;
    }

    /**
     * Check the database for a user with the same username.
     *
     * @param {string} [$data] - The string being tested.
     * @return {bool}
     */
    public function usernameExists( $data ) {
        if ( Forms::checkUsername( $data ) ) {
            $usernameResults = self::$db->get( $this->tableName, [ 'username', '=', $data ] );
            if ( $usernameResults->count() ) {
                return true;
            }
        }
        // self::addError("No user exists in the DB.", $data);
        return false;
    }

    /**
     * Checks username formatting.
     *
     * Requirements:
     * - 4 - 16 characters long
     * - must only contain numbers and letters: [A - Z] , [a - z], [0 - 9]
     *
     * @param {string} [$data] - The string being tested.
     * @return {bool}
     */
    public function checkUsername( $data ) {
        if ( strlen( $data ) > 16 ) {
            // self::addError("Username must be be 4 to 16 numbers or letters.", $data);
            return false;
        }
        if ( strlen( $data ) < 4 ) {
            // self::addError("Username must be be 4 to 16 numbers or letters.", $data);
            return false;
        }
        if ( !ctype_alnum( $data ) ) {
            // self::addError("Username must be be 4 to 16 numbers or letters.", $data);
            return false;
        }
        return true;
    }

    /**
     * Find and define usernames by user ID.
     *
     * @param {int} [$id] - The ID of the user you are looking for.
     * @return {string} - Either the username or 'unknown' will be returned.
     */
    public function getUsername( $id ) {
        if ( !Check::id( $id ) ) {
            return false;
        }
        if ( !isset( self::$usernames[ $id ] ) ) {
            $user = $this->get( $id );
            if ( $user !== false ) {
                self::$usernames[ $id ] = $user->username;
            } else {
                self::$usernames[ $id ] = 'Unknown';
            }
        }
        return self::$usernames[ $id ];
    }

    /**
     * Since we need a cache of the usernames, we use this function
     * to find/return all usernames based on ID.
     *
     * @param {int} [$username] - The username of the user you are looking for.
     * @return {int}
     */
    public function getID( $username ) {
        if ( !Forms::checkUsername( $username ) ) {
            return false;
        }
        $user = $this->get( $username );
        if ( $user !== false ) {
            return $user->ID;
        } else {
            return 0;
        }
    }

    /**
     * Find and define user avatar image urls.
     *
     * @param {int} [$id] - The ID of the user you are looking for.
     * @return {string} - Either the username or 'unknown' will be returned.
     */
    public function getAvatar( $id ) {
        if ( !Check::id( $id ) ) {
            return false;
        }
        if ( !isset( self::$avatars[ $id ] ) ) {
            if ( $this->get( $id ) ) {
                self::$avatars[ $id ] = self::data()->avatar;
            } else {
                self::$avatars[ $id ] = '{BASE}images/defaultAvatar.png';
            }
        }
        return self::$avatars[ $id ];
    }

    /**
     * Delete the specified user(s).
     *
     * @param {int|array} [$data] - The log ID or array of ID's to be deleted.
     * @return {bool}
     */
    public function delete( $idArray ) {
        if ( !is_array( $idArray ) ) {
            $idArray = [ $idArray ];
        }
        foreach ( $idArray as $id ) {
            if ( App::$activeUser->ID == $id ) {
                Debug::info( 'Attempting to delete own account.' );
                return false;
            }
            $user = $this->get( $id );
            if (
                'Super' == $user->groupName 
                && 'Super' !== App::$activeGroup->name
             ) {
                Debug::info( 'Attempting to delete superior account.' );
                return false;
            }
        }
        return parent::delete( $idArray );
    }

    /**
     * Attempt to authenticate a user login and set them as the active user.
     *
     * @param {string} [$username] - The username being used to login.
     * @param {string} [$password] - The un-hashed password.
     * @param {bool} [$remember] - Whether the user wishes to be remembered or not.
     * @return {bool}
     */
    public function logIn( $username, $password, $remember = false ) {
        if ( !isset( self::$session ) ) {
            self::$session = new Sessions;
        }
        if ( !isset( self::$log ) ) {
            self::$log = new Log;
        }
        Debug::group( 'login', 1 );
        if ( !Forms::checkUsername( $username ) ) {
            Debug::warn( 'Invalid Username.' );
            return false;
        }
        if ( !$this->get( $username ) ) {
            self::$log->login( 0, "User not found: $username" );
            Debug::warn( "User not found: $username" );
            return false;
        }
        // login attempts protection.
        $timeLimit = ( time() - 3600 );
        $limit = Config::getValue( 'main/loginLimit' );
        $user = $this->data();
        if ( $limit > 0 ) {
            $limitCheck = self::$db->get(
                'logs',
                [
                    'source', '=', 'login',
                    'AND',
                    'userID', '=', $user->ID,
                    'AND',
                    'time', '>=', $timeLimit,
                    'AND',
                    'action', '!=', 'pass',
                ]
            );
            if ( $limitCheck->count() >= $limit ) {
                Debug::info( 'login: Limit reached.', 1 );
                self::$log->login( $user->ID, 'Too many failed attempts.' );
                Debug::warn( 'Too many failed login attempts, please try again later.' );
                return false;
            }
        }
        if ( !Check::password( $password ) ) {
            Debug::warn( 'Invalid password.' );
            self::$log->login( $user->ID, 'Invalid Password.' );
            return false;
        }
        if ( !Hash::check( $password, $user->password ) ) {
            Debug::warn( 'Pass hash does not match.' );
            self::$log->login( $user->ID, 'Wrong Password.' );
            return false;
        }
        self::$session->newSession( null, true, $remember, $user->ID );
        self::$log->login( $this->data()->ID, 'pass' );
        $this->update( $this->data()->ID, [ 'lastLogin' => time() ] );
        Debug::gend();
        return true;
    }

    /**
     * Log out the currently active user.
     */
    public function logOut() {
        if ( !isset( self::$session ) ) {
            self::$session = new Sessions;
        }
        Debug::group( 'Logout', 1 );
        self::$session->destroy( Session::get( 'SessionToken' ) );
        App::$isLoggedIn = false;
        App::$isMember = false;
        App::$isMod = false;
        App::$isAdmin = false;
        App::$activeUser = null;
        Debug::info( 'User has been logged out.' );
        Debug::gend();
        return null;
    }

    /**
     * Change a user password.
     *
     * @param {string} [$code] - The confirmation code required from the password email.
     * @param {string} [$password] - The new password for the user's account.
     * @return {bool}
     */
    public function changePassword( $code, $password ) {
        if ( !Check::password( $password ) ) {
            return false;
        }
        $data = self::$db->get( $this->tableName, [ 'confirmationCode', '=', $code ] );
        if ( $data->count() ) {
            $this->data = $data->first();
            $this->update(
                $this->data->ID,
                [ 'password' => Hash::make( $password ), 'confirmationCode' => '', ],
            );
            return true;
        }
        return false;
    }

    /**
     * Create a list of registered users.
     *
     * @param {array} [$filter] - A filter to be applied to the users list.
     * @return {bool|object}
     */
    public function userList( $filter = null ) {
        if ( ! empty( $filter ) ) {
            switch ( $filter ) {
                case 'newsletter':
                    $data = self::$db->search( $this->tableName, 'prefs', 'newsletter":"true' );
                    break;
                default:
                    $data = self::$db->get( $this->tableName, '*' );
                    break;
            }
        } else {
            $data = self::$db->get( $this->tableName, '*' );
        }
        if ( ! $data->count() ) {
            return false;
        }
        return (object) $data->results();
    }

    /**
     * Create a list of recently registered users.
     *
     * @param {int} [$limit] - How many posts you would like returned.
     * @return {bool|object}
     */
    public function recent( $limit = null ) {
        if ( empty( $limit ) ) {
            $data = self::$db->get( $this->tableName, '*' );
        } else {
            $data = self::$db->get( $this->tableName, [ 'ID', '>', '0' ], 'ID', 'DESC', [ 0, $limit ] );
        }
        if ( !$data->count() ) {
            return false;
        }
        return (object) $data->results();
    }

    /**
     * Check the database for a user with the same confirmation code.
     *
     * @param {string} [$code] - The confirmation code being checked.
     * @return {bool}
     */
    public function checkCode( $code ) {
        $data = self::$db->get( $this->tableName, [ 'confirmationCode', '=', $code ] );
        if ( $data->count() > 0 ) {
            return true;
        }
        Debug::error( 'User confirmation code not found.' );
        return false;
    }

    /**
     * Generate and save a new confirmation code for the user.
     *
     * @param {int} [$id] - The user ID to update the confirmation code for.
     * @return {bool}
     */
    public function newCode( $id ) {
        $data = self::$db->get( $this->tableName, [ 'ID', '=', $id ] );
        if ( $data->count() == 0 ) {
            return false;
        }
        $this->data = $data->first();
        $Ccode = md5( uniqid() );
        $this->update(
            $this->data->ID,
            [ 'confirmationCode' => $Ccode ],
        );

        return true;
    }

    /**
     * Finds and confirms a user by their confirmation code.
     *
     * @param {string} [$code] - The confirmation code sent to the user.
     * @return {bool}
     */
    public function confirm( $code ) {
        $data = self::$db->get( $this->tableName, [ 'confirmationCode', '=', $code ] );
        if ( $data->count() ) {
            $this->data = $data->first();
            $this->update(
                $this->data->ID,
                [ 'confirmed' => 1, 'confirmationCode' => '', ],
            );
            return true;
        }
        return false;
    }

    /**
     * Check if the specified user exists or not.
     *
     * @return {bool}
     * @todo this function should actually check for a user
     */
    public function exists() {
        return ( !empty( $this->data ) ) ? true : false;
    }

    public function filter( $data, $params = [] ) {
        foreach ( $data as $instance ) {
            if ( !is_object( $instance ) ) {
                $instance = (object) $data;
                $end = true;
            }
            if ( $instance->confirmed == 1 ) {
                $instance->confirmedText = 'Yes';
            } else {
                $instance->confirmedText = 'No';
            }
            $group = self::$group->findById( $instance->userGroup );
            if ( !empty( $group ) ) {
                $instance->groupName = $group->name;
            } else {
                $instance->groupName = 'Unknown';
            }
            $instance->prefs = json_decode( $instance->prefs, true );
            $instance->gender = $instance->prefs['gender'];
            $instance->avatar = $instance->prefs['avatar'];
            $out[] = $instance;
            if ( !empty( $end ) ) {
                $out = $out[0];
                break;
            }
        }
        return $out;
    }

    /**
     * Get user data from an ID or username.
     *
     * @param {int|string} [$user] - Either the username or user ID being searched for.
     * @return {bool|array}
     */
    public function get( $user ) {
        if ( empty( self::$group ) ) {
            self::$group = new Group;
        }
        $user = (string) $user;
        $field = ( ctype_digit( $user ) ) ? 'ID' : 'username';
        if ( $field == 'username' ) {
            if ( !Forms::checkUsername( $user ) ) {
                Debug::info( 'modelUser->get Username improperly formatted.' );
                return false;
            }
        } else {
            if ( !Check::id( $user ) ) {
                Debug::info( 'modelUser->get Invalid ID.' );
                return false;
            }
        }
        $data = self::$db->get( $this->tableName, [ $field, '=', $user ] );
        if ( !$data->count() ) {
            Debug::info( "modelUser->get User not found: $user" );
            return false;
        }
        $this->data = $this->filter( $data->first() );
        return $this->data;
    }

    /**
     * Find a user by email address.
     *
     * @param {string} [$email] - The email being searched for.
     * @return {bool}
     */
    public function findByEmail( $email ) {
        if ( Check::email( $email ) ) {
            $data = self::$db->get( $this->tableName, [ 'email', '=', $email ] );
            if ( $data->count() ) {
                $this->data = $data->first();
                return true;
            }
        }
        Debug::error( "modelUser->findByEmail - User not found by email: $email" );
        return false;
    }

    /**
     * Create a new user.
     *
     * @param {array} [$fields] - The New User's data.
     * @return {bool}
     */
    public function create( $fields = [] ) {
        if ( empty( $fields ) ) {
            return false;
        }
        if ( !isset( $fields['email' ] ) ) {
            return false;
        }
        if ( !isset( $fields['prefs' ] ) ) {
            $fields['prefs'] = json_encode( $this->getDefaultPreferences() );
        }
        if ( !isset( $fields['userGroup' ] ) ) {
            $fields['userGroup'] = Config::getValue( 'group/defaultGroup' );
        } else {
            if ( in_array( $fields['userGroup'], [ '1', 1 ] ) ) {
                if ( App::$activeGroup && 'Super' !== App::$activeGroup->name ) {
                    Debug::error( 'You do not have permission to do this.' );
                }
            }
        }
        if ( !isset( $fields['registered' ] ) ) {
            $fields['registered'] = time();
        }
        if ( !isset( $fields['confirmed' ] ) ) {
            $code = Code::genConfirmation();
            $fields['confirmed'] = 0;
            $fields['confirmationCode'] = $code;
            Email::send( $fields['email'], 'confirmation', $code, [ 'template' => true ] );
        }
        if ( !self::$db->insert( $this->tableName, $fields ) ) {
            Debug::error( 'User not created.' );
            return false;
        }
        return true;
    }

    /**
     * Update a user database entry.
     *
     * @param {array} [$fields] - The fields to be updated.
     * @param {int} [$id] - The user ID being updated.
     * @return {bool}
     */
    public function update( $id, $fields = [] ) {
        if ( !Check::id( $id ) ) {
            return false;
        }
        if ( !self::$db->update( $this->tableName, $id, $fields ) ) {
            new CustomException( 'userUpdate' );
            Debug::error( "User: $id not updated: $fields" );
            return false;
        }
        return true;
    }

    /**
     * Update a user's preferences.
     *
     * @param {array} [$fields] - The fields to be updated.
     * @param {int} [$id] - The user ID being updated.
     * @return {bool}
     */
    public function updatePrefs( $fields, $id ) {
        if ( !Check::id( $id ) ) {
            return false;
        }
        $userData = $this->get( $id );
        $prefsInput = $userData->prefs;
        foreach ( $fields as $name => $value ) {
            $prefsInput[$name] = $value;
        }
        $fields = [ 'prefs' => json_encode( $prefsInput ) ];
        if ( !self::$db->update( $this->tableName, $id, $fields ) ) {
            Debug::error( "User: $id not updated." );
            return false;
        }
        if ( $id === App::$activeUser->ID ) {
            $userData = $this->get( $id );
            App::$activeUser = $userData;
        }
        return true;
    }

    /**
     * Return the most recent database data.
     *
     * @return {array} - An array of the user data.
     */
    public function data() {
        return $this->data;
    }

    public function authorize( $username, $password ) {
        if ( !isset( self::$log ) ) {
            self::$log = new Log;
        }
        if ( !$this->get( $username ) ) {
            self::$log->login( 0, "API: User not found: $username" );
            return false;
        }
        // login attempts protection.
        $timeLimit = ( time() - 3600 );
        $limit = Config::getValue( 'main/loginLimit' );
        $user = $this->data();
        if ( $limit > 0 ) {
            $limitCheck = self::$db->get(
                'logs',
                [
                    'source', '=', 'login',
                    'AND',
                    'userID', '=', $user->ID,
                    'AND',
                    'time', '>=', $timeLimit,
                    'AND',
                    'action', '!=', 'pass',
                ]
            );
            if ( $limitCheck->count() >= $limit ) {
                self::$log->login( $user->ID, 'API: Too many failed attempts.' );
                return false;
            }
        }
        if ( !Check::password( $password ) ) {
            self::$log->login( $user->ID, 'API: Invalid Password.' );
            return false;
        }
        if ( !Hash::check( $password, $user->password ) ) {
            self::$log->login( $user->ID, 'API: Wrong Password.' );
            return false;
        }
        self::$log->login( $this->data()->ID, 'API: pass' );
        return $user;
    }
}
