<?php
namespace Admidio\Users\Entity;

use Admidio\Infrastructure\Database;
use Admidio\Infrastructure\Entity\Entity;
use Admidio\Infrastructure\Exception;
use Admidio\Infrastructure\SystemMail;
use Admidio\Infrastructure\Utils\SecurityUtils;
use Admidio\ProfileFields\ValueObjects\ProfileFields;

/**
 * @brief Creates, assign and update user registrations in database
 *
 * This class extends the User class with some special functions for new registrations.
 * If a new user is saved than there will be an additional table entry in the
 * registration table. This entry must be deleted if a registration is confirmed
 * or deleted. If a registration is confirmed or deleted then a notification SystemMail
 * will be sent to the user. If email couldn't be sent than an Exception will be thrown.
 *
 * **Code example**
 * ```
 * // create a valid registration
 * $user = new UserRegistration($gDb, $gProfileFields);
 * $user->setValue('LAST_NAME', 'Schmidt');
 * $user->setValue('FIRST_NAME', 'Franka');
 * ...
 * // save user data and create registration
 * $user->save();
 * ```
 *
 * **Code example**
 * ```
 * // assign a registration
 * $userId = 4711;
 * $user = new UserRegistration($gDb, $gProfileFields, $userId);
 * // set user to valid and send notification email
 * $user->acceptRegistration();
 * ```
 * @copyright The Admidio Team
 * @see https://www.admidio.org/
 * @license https://www.gnu.org/licenses/gpl-2.0.html GNU General Public License v2.0 only
 */
class UserRegistration extends User
{
    /**
     * @var bool Flag if the object will send a SystemMail if registration is accepted or deleted.
     */
    private bool $sendEmail = true;
    /**
     * @var Entity
     */
    private Entity $tableRegistration;

    /**
     * Constructor that will create an object of a recordset of the users table.
     * If the id is set than this recordset will be loaded.
     * @param Database $database Object of the class Database. This should be the default global object **$gDb**.
     * @param ProfileFields $userFields An object of the ProfileFields class with the profile field structure
     *                                      of the current organization. This could be the default object .
     * @param int $userId The id of the user who should be loaded. If id isn't set than an empty object
     *                                      with no specific user is created.
     * @param int $organizationId The id of the organization for which the user should be registered.
     *                                      If no id is set than the user will be registered for the current organization.
     * @throws Exception
     */
    public function __construct(Database $database, ProfileFields $userFields, int $userId = 0, int $organizationId = 0)
    {
        parent::__construct($database, $userFields, $userId);

        // changes to a registration user should not be relevant for the change notifications
        $this->disableChangeNotification();

        if ($organizationId > 0) {
            $this->setOrganization($organizationId);
        }

        // create recordset for registration table
        $this->tableRegistration = new Entity($this->db, TBL_REGISTRATIONS, 'reg');
        $this->tableRegistration->readDataByColumns(array('reg_org_id' => $this->organizationId, 'reg_usr_id' => $userId));
    }

    /**
     * Deletes the registration record and set the user to valid. The user will also be assigned to all roles
     * that have the flag **rol_default_registration**. After that a notification email is send to the user.
     * If function returns **true** than the user can log in for the organization of this object.
     * @return true Returns **true** if the registration was successful
     * @throws Exception
     */
    public function acceptRegistration(): bool
    {
        global $gSettingsManager, $gMenu;

        $this->db->startTransaction();

        // set user active
        $this->saveChangesWithoutRights();
        $this->setValue('usr_valid', 1);
        $this->save();

        // delete registration record in registration table
        $this->tableRegistration->delete();

        // every user will get the default roles for registration
        $this->assignDefaultRoles();

        $this->db->endTransaction();

        // update registration count in menu
        $gMenu->initialize();

        // only send mail if systemmails are enabled
        if ($gSettingsManager->getBool('system_notifications_enabled')
        && $gSettingsManager->getBool('registration_manual_approval') && $this->sendEmail) {
            // send mail to user that his registration was accepted
            $sysMail = new SystemMail($this->db);
            $sysMail->addRecipientsByUser($this->getValue('usr_uuid'));
            $sysMail->sendSystemMail('SYSMAIL_REGISTRATION_APPROVED', $this); // TODO Exception handling
        }

        return true;
    }

    /**
     * Method will adopt the profile fields data of this user object to the user object of the parameters.
     * If the system setting **registration_adopt_all_data** is set than all data of the registration process will
     * be adopted otherwise only username and password will be added to the existing user. The adopted data of the
     * user will not be saved. That must be done in the calling method because of duplicate **usr_login_name**.
     * @param User $user Object of the existing user that should be adopted.
     * @throws Exception
     */
    public function adoptUser(User $user)
    {
        // always adopt login name and password to the destination user
        $user->setValue('usr_login_name', $this->getValue('usr_login_name'));
        $user->setPassword($this->getValue('usr_password'), false);

        // adopt all registration fields to the user if this is enabled in the settings
        if ($GLOBALS['gSettingsManager']->getBool('registration_adopt_all_data')) {
            foreach ($this->mProfileFieldsData->getProfileFields() as $profileField) {
                if ((string) $this->mProfileFieldsData->getValue($profileField->getValue('usf_name_intern')) !== '') {
                    $user->setValue($profileField->getValue('usf_name_intern'), $this->mProfileFieldsData->getValue($profileField->getValue('usf_name_intern'), 'database'));
                }
            }
        }
    }

    /**
     * Deletes the selected user registration. If user is not valid and has no other registrations than
     * delete user because he has no use for the system. After that a notification email is send to the user.
     * If the user is valider than only the registration will be deleted!
     * @return bool **true** if no error occurred
     * @throws Exception
     */
    public function delete(): bool
    {
        global $gMenu;

        // only send mail if systemmails are enabled and user has email address
        // mail must be sent before user data is removed from this object
        if ($GLOBALS['gSettingsManager']->getBool('system_notifications_enabled') && $this->sendEmail && $this->getValue('EMAIL') !== '') {
            // send mail to user that his registration was rejected
            $sysMail = new SystemMail($this->db);
            $sysMail->addRecipientsByUser($this->getValue('usr_uuid'));
            $sysMail->sendSystemMail('SYSMAIL_REGISTRATION_REFUSED', $this); // TODO Exception handling
        }

        $this->db->startTransaction();

        // delete registration record in registration table
        $return = $this->tableRegistration->delete();

        // if user is not valid and has no other registrations
        // than delete user because he has no use for the system
        if (!$this->getValue('usr_valid')) {
            $sql = 'SELECT reg_id
                      FROM '.TBL_REGISTRATIONS.'
                     WHERE reg_usr_id = ? -- $this->getValue(\'usr_id\')';
            $registrationsStatement = $this->db->queryPrepared($sql, array((int) $this->getValue('usr_id')));

            if ($registrationsStatement->rowCount() === 0) {
                $return = parent::delete();
            }
        }

        $this->db->endTransaction();

        // update registration count in menu
        $gMenu->initialize();

        return $return;
    }

    /**
     * Send a notification email to all role members of roles that can approve registrations
     * therefore the flags system mails and notification mail for roles with approve registration must be activated
     * @throws Exception
     */
    public function notifyAuthorizedMembers()
    {
        global $gSettingsManager;

        if ($gSettingsManager->getBool('system_notifications_enabled')
            && $gSettingsManager->getBool('registration_send_notification_email') && $this->sendEmail) {
            $sql = 'SELECT rol_uuid
                      FROM '.TBL_ROLES.'
                INNER JOIN '.TBL_CATEGORIES.'
                        ON cat_id = rol_cat_id
                     WHERE rol_approve_users = true
                       AND rol_valid = true
                       AND cat_org_id = ? -- $this->organizationId';
            $rolesStatement = $this->db->queryPrepared($sql, array($this->organizationId));

            while ($row = $rolesStatement->fetch()) {
                // send mail that a new registration is available
                $sysMail = new SystemMail($this->db);
                $sysMail->addRecipientsByRole($row['rol_uuid']);
                $sysMail->sendSystemMail('SYSMAIL_REGISTRATION_NEW', $this); // TODO Exception handling
            }
        }
    }

    /**
     * If called than the object will not send a SystemMail when registration was accepted or deleted.
     */
    public function notSendEmail()
    {
        $this->sendEmail = false;
    }

    /**
     * Reads a record out of the table in database selected by the unique uuid column in the table.
     * The name of the column must have the syntax table_prefix, underscore and uuid. E.g. usr_uuid.
     * Per default all columns of the default table will be read and stored in the object.
     * Not every Admidio table has an uuid. Please check the database structure before you use this method.
     * @param string $uuid Unique uuid that should be searched.
     * @return bool Returns **true** if one record is found
     * @throws Exception
     * @see Entity#readDataByColumns
     * @see Entity#readData
     */
    public function readDataByUuid(string $uuid): bool
    {
        $returnValue = parent::readDataByUuid($uuid);

        // create recordset for registration table
        $this->tableRegistration = new Entity($this->db, TBL_REGISTRATIONS, 'reg');
        $this->tableRegistration->readDataByColumns(array('reg_org_id' => $GLOBALS['gCurrentOrganization']->getValue('org_id'),
            'reg_usr_id' => $this->getValue('usr_id')));

        return $returnValue;
    }

    /**
     * Save all changed columns of the recordset in table of database. If it's a new user
     * than the registration table will also be filled with a new recordset and optional a
     * notification mail will be sent to all users of roles that have the right to confirm registrations
     * @param bool $updateFingerPrint Default **true**. Will update the creator or editor of the recordset
     *                                if table has columns like **usr_id_create** or **usr_id_changed**
     * @return bool
     * @throws Exception
     * @throws \Exception
     */
    public function save(bool $updateFingerPrint = true): bool
    {
        global $gSettingsManager;

        // if new registration is saved then set user not valid
        if ($this->tableRegistration->isNewRecord()) {
            $this->setValue('usr_valid', 0);
        }

        $returnValue = parent::save($updateFingerPrint); // TODO Exception handling

        // if new registration is saved then save also record in registration table and send notification mail
        if ($this->tableRegistration->isNewRecord()) {
            // create a unique validation id
            $validationId = SecurityUtils::getRandomString(50);

            // save registration record
            $this->tableRegistration->setValue('reg_org_id', $this->organizationId);
            $this->tableRegistration->setValue('reg_usr_id', (int) $this->getValue('usr_id'));
            $this->tableRegistration->setValue('reg_timestamp', DATETIME_NOW);
            $this->tableRegistration->setValue('reg_validation_id', $validationId);
            $this->tableRegistration->save();

            // send a notification mail to the user to confirm his registration
            if ($gSettingsManager->getBool('system_notifications_enabled') && $this->sendEmail) {
                $sysMail = new SystemMail($this->db);
                $sysMail->addRecipientsByUser($this->getValue('usr_uuid'));
                $sysMail->setVariable(1, SecurityUtils::encodeUrl(ADMIDIO_URL.FOLDER_MODULES.'/registration.php', array('user_uuid' => $this->getValue('usr_uuid'), 'id' => $validationId)));
                $sysMail->sendSystemMail('SYSMAIL_REGISTRATION_CONFIRMATION', $this); // TODO Exception handling
            }
        }

        return $returnValue;
    }

    /**
     * Method will search in the registration table for a valid registration for this user in combination with the
     * validation ID. If a valid registration was found than reg_validation_id will be cleared and so the
     * registration is validated through the user. After that the registration must be manually approved by a member
     * of the organization.
     * @param string $validationId A validation ID of the registration that should be checked.
     * @return bool Return **true** if the given data could be joined to a valid registration
     * @throws Exception
     */
    public function validate(string $validationId): bool
    {
        $sql = 'SELECT reg.*
                  FROM ' . TBL_REGISTRATIONS . ' reg
                 WHERE reg_usr_id = ? -- $this->getValue(\'usr_id\')
                   AND reg_validation_id = ? -- $validationId ';
        $queryParams = array(
            $this->getValue('usr_id'),
            $validationId
        );
        $registrationStatement = $this->db->queryPrepared($sql, $queryParams);

        if ($registrationStatement->rowCount() === 1) {
            $row = $registrationStatement->fetch();
            $registration = new Entity($this->db, TBL_REGISTRATIONS, 'reg');
            $registration->setArray($row);

            // registration ID is only valid for 1 day
            $timeGap = time() - strtotime($row['reg_timestamp']);

            if ($timeGap < 60 * 60 * 24) {
                $registration->setValue('reg_validation_id', null);
                $registration->save();
                return true;
            }
        }

        return false;
    }
}
