<?php
/*
 * Copyright (c) 2022 Xibo Signage Ltd
 *
 * Xibo - Digital Signage - http://www.xibo.org.uk
 *
 * This file is part of Xibo.
 *
 * Xibo is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * any later version.
 *
 * Xibo is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with Xibo.  If not, see <http://www.gnu.org/licenses/>.
 */


namespace Xibo\Entity;


use Respect\Validation\Validator as v;
use Xibo\Factory\UserFactory;
use Xibo\Factory\UserGroupFactory;
use Xibo\Service\LogServiceInterface;
use Xibo\Storage\StorageServiceInterface;
use Xibo\Support\Exception\DuplicateEntityException;
use Xibo\Support\Exception\InvalidArgumentException;
use Xibo\Support\Exception\NotFoundException;

/**
 * Class UserGroup
 * @package Xibo\Entity
 *
 * @SWG\Definition()
 */
class UserGroup
{
    use EntityTrait;

    /**
     * @SWG\Property(description="The Group ID")
     * @var int
     */
    public $groupId;

    /**
     * @SWG\Property(description="The group name")
     * @var string
     */
    public $group;

    /**
     * @SWG\Property(description="A flag indicating whether this is a user specific group or not")
     * @var int
     */
    public $isUserSpecific = 0;

    /**
     * @SWG\Property(description="A flag indicating the special everyone group")
     * @var int
     */
    public $isEveryone = 0;

    /**
     * @SWG\Property(description="Description of this User Group")
     * @var string
     */
    public $description;

    /**
     * @SWG\Property(description="This users library quota in bytes. 0 = unlimited")
     * @var int
     */
    public $libraryQuota;

    /**
     * @SWG\Property(description="Does this Group receive system notifications.")
     * @var int
     */
    public $isSystemNotification = 0;

    /**
     * @SWG\Property(description="Does this Group receive display notifications.")
     * @var int
     */
    public $isDisplayNotification = 0;

    /**
     * @SWG\Property(description="Is this Group shown in the list of choices when onboarding a new user")
     * @var int
     */
    public $isShownForAddUser = 0;

    /**
     * @SWG\Property(description="Default Home page for new users")
     * @var string
     */
    public $defaultHomepageId;

    /**
     * @SWG\Property(description="Features this User Group has direct access to", @SWG\Items(type="string"))
     * @var array
     */
    public $features = [];

    // Users
    private $users = [];

    /**
     * @var UserGroupFactory
     */
    private $userGroupFactory;

    /**
     * @var UserFactory
     */
    private $userFactory;

    private $assignedUserIds = [];
    private $unassignedUserIds = [];

    /**
     * Entity constructor.
     * @param StorageServiceInterface $store
     * @param LogServiceInterface $log
     * @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $dispatcher
     * @param UserGroupFactory $userGroupFactory
     * @param UserFactory $userFactory
     */
    public function __construct($store, $log, $dispatcher, $userGroupFactory, $userFactory)
    {
        $this->setCommonDependencies($store, $log, $dispatcher);

        $this->userGroupFactory = $userGroupFactory;
        $this->userFactory = $userFactory;
    }

    /**
     *
     */
    public function __clone()
    {
        // Clear the groupId
        $this->groupId = null;
    }

    /**
     * @return string
     */
    public function __toString()
    {
        return sprintf('ID = %d, Group = %s, IsUserSpecific = %d', $this->groupId, $this->group, $this->isUserSpecific);
    }

    /**
     * Generate a unique hash for this User Group
     */
    private function hash()
    {
        return md5(json_encode($this));
    }

    /**
     * @return int
     */
    public function getId()
    {
        return $this->groupId;
    }

    /**
     * @return int
     */
    public function getOwnerId()
    {
        return 0;
    }

    /**
     * Set the Owner of this Group
     * @param User $user
     */
    public function setOwner($user)
    {
        $this->load();

        $this->isUserSpecific = 1;
        $this->isEveryone = 0;
        $this->assignUser($user);
    }

    /**
     * Assign User
     * @param User $user
     */
    public function assignUser($user)
    {
        $this->load();

        if (!in_array($user, $this->users)) {
            $this->users[] = $user;
            $this->assignedUserIds[] = $user->userId;
        }
    }

    /**
     * Unassign User
     * @param User $user
     */
    public function unassignUser($user)
    {
        $this->load();
        $this->unassignedUserIds[] = $user->userId;
        $this->users = array_udiff($this->users, [$user], function($a, $b) {
            /**
             * @var User $a
             * @var User $b
             */
            return $a->getId() - $b->getId();
        });
    }

    /**
     * Validate
     */
    public function validate()
    {
        if (!v::stringType()->length(1, 50)->validate($this->group))
            throw new InvalidArgumentException(__('User Group Name cannot be empty.') . $this, 'name');

        if ($this->libraryQuota !== null && !v::intType()->validate($this->libraryQuota))
            throw new InvalidArgumentException(__('Library Quota must be a whole number.'), 'libraryQuota');

        try {
            $group = $this->userGroupFactory->getByName($this->group, $this->isUserSpecific);

            if ($this->groupId == null || $this->groupId != $group->groupId)
                throw new DuplicateEntityException(__('There is already a group with this name. Please choose another.'));
        }
        catch (NotFoundException $e) {

        }
    }

    /**
     * Load this User Group
     * @param array $options
     */
    public function load($options = [])
    {
        $options = array_merge([
            'loadUsers' => true
        ], $options);

        if ($this->loaded || $this->groupId == 0) {
            return;
        }

        if ($options['loadUsers']) {
            // Load all assigned users
            $this->users = $this->userFactory->getByGroupId($this->groupId);
        }

        // Set the hash
        $this->hash = $this->hash();
        $this->loaded = true;
    }

    /**
     * Save the group
     * @param array $options
     * @throws DuplicateEntityException
     * @throws InvalidArgumentException
     */
    public function save($options = [])
    {
        $options = array_merge([
            'validate' => true,
            'linkUsers' => true
        ], $options);

        if ($options['validate'])
            $this->validate();

        if ($this->groupId == null || $this->groupId == 0) {
            $this->add();
            $this->audit($this->groupId, 'User Group added', ['group' => $this->group]);
        } else if ($this->hash() != $this->hash) {
            $this->edit();
            $this->audit($this->groupId, 'User Group edited');
        }

        if ($options['linkUsers']) {
            $this->linkUsers();
            $this->unlinkUsers();

            if (count($this->assignedUserIds) > 0) {
                $this->audit($this->groupId, 'Users assigned', ['userIds' => implode(',', $this->assignedUserIds)]);
            }

            if (count($this->unassignedUserIds) > 0) {
                $this->audit($this->groupId, 'Users unassigned', ['userIds' => implode(',', $this->unassignedUserIds)]);
            }
        }
    }

    /**
     * Save features
     * @return $this
     */
    public function saveFeatures()
    {
        $this->getStore()->update('
            UPDATE `group` SET features = :features WHERE groupId = :groupId
        ', [
            'groupId' => $this->groupId,
            'features' => json_encode($this->features)
        ]);

        $this->audit($this->groupId, 'User Group feature access modified', [
            'features' => json_encode($this->features)
        ]);

        return $this;
    }

    /**
     * Delete this Group
     */
    public function delete()
    {
        // We must ensure everything is loaded before we delete
        if ($this->hash == null) {
            $this->load();
        }

        // Unlink users
        $this->removeAssignments();

        $this->getStore()->update('DELETE FROM `permission` WHERE groupId = :groupId', ['groupId' => $this->groupId]);
        $this->getStore()->update('DELETE FROM `group` WHERE groupId = :groupId', ['groupId' => $this->groupId]);

        $this->audit($this->groupId, 'User group deleted.', false);
    }

    /**
     * Remove all assignments
     */
    private function removeAssignments()
    {
        // Delete Notifications
        // NB: notifications aren't modelled as child objects because there could be many thousands of notifications on each
        // usergroup. We consider the notification to be the parent here and it manages the assignments.
        // This does mean that we might end up with an empty notification (not assigned to anything)
        $this->getStore()->update('DELETE FROM `lknotificationuser` WHERE `userId` IN (SELECT `userId` FROM `lkusergroup` WHERE `groupId` = :groupId) ', ['groupId' => $this->groupId]);
        $this->getStore()->update('DELETE FROM `lknotificationgroup` WHERE `groupId` = :groupId', ['groupId' => $this->groupId]);

        // Remove user assignments
        $this->users = [];
        $this->unlinkUsers();
    }

    /**
     * Add
     */
    private function add()
    {
        $this->groupId = $this->getStore()->insert('
            INSERT INTO `group` (
                 `group`, 
                 IsUserSpecific,
                 `description`,
                 libraryQuota, 
                 `isSystemNotification`, 
                 `isDisplayNotification`,
                 `isShownForAddUser`,
                 `defaultHomepageId`
              )
              VALUES (
                      :group, 
                      :isUserSpecific, 
                      :description, 
                      :libraryQuota, 
                      :isSystemNotification, 
                      :isDisplayNotification,
                      :isShownForAddUser,
                      :defaultHomepageId
              )
        ', [
            'group' => $this->group,
            'isUserSpecific' => $this->isUserSpecific,
            'description' => $this->description,
            'libraryQuota' => $this->libraryQuota,
            'isSystemNotification' => $this->isSystemNotification,
            'isDisplayNotification' => $this->isDisplayNotification,
            'isShownForAddUser' => $this->isShownForAddUser,
            'defaultHomepageId' => $this->defaultHomepageId
        ]);
    }

    /**
     * Edit
     */
    private function edit()
    {
        $this->getStore()->update('
          UPDATE `group` SET 
            `group` = :group, 
            `description` = :description, 
            libraryQuota = :libraryQuota, 
            `isSystemNotification` = :isSystemNotification,
            `isDisplayNotification` = :isDisplayNotification,
            `isShownForAddUser` = :isShownForAddUser,
            `defaultHomepageId` = :defaultHomepageId
           WHERE groupId = :groupId
        ', [
            'groupId' => $this->groupId,
            'group' => $this->group,
            'description' => $this->description,
            'libraryQuota' => $this->libraryQuota,
            'isSystemNotification' => $this->isSystemNotification,
            'isDisplayNotification' => $this->isDisplayNotification,
            'isShownForAddUser' => $this->isShownForAddUser,
            'defaultHomepageId' => $this->defaultHomepageId
        ]);
    }

    /**
     * Link Users
     */
    private function linkUsers()
    {
        $insert = $this->getStore()->getConnection()->prepare('INSERT INTO `lkusergroup` (groupId, userId) VALUES (:groupId, :userId) ON DUPLICATE KEY UPDATE groupId = groupId');

        foreach ($this->users as $user) {
            /* @var User $user */
            $this->getLog()->debug('Linking %s to %s', $user->userName, $this->group);

            $insert->execute([
                'groupId' => $this->groupId,
                'userId' => $user->userId
            ]);
        }
    }

    /**
     * Unlink Users
     */
    private function unlinkUsers()
    {
        $params = ['groupId' => $this->groupId];

        $sql = 'DELETE FROM `lkusergroup` WHERE groupId = :groupId AND userId NOT IN (0';

        $i = 0;
        foreach ($this->users as $user) {
            /* @var User $user */
            $i++;
            $sql .= ',:userId' . $i;
            $params['userId' . $i] = $user->userId;
        }

        $sql .= ')';

        $this->getStore()->update($sql, $params);
    }
}
