<?php

/*
 * Copyright (c) 2010-2022 Tim Düsterhus.
 *
 * Use of this software is governed by the Business Source License
 * included in the LICENSE file.
 *
 * Change Date: 2026-03-10
 *
 * On the date above, in accordance with the Business Source
 * License, use of this software will be governed by version 2
 * or later of the General Public License.
 */

namespace chat\data\room;

use chat\data\command\CommandCache;
use chat\data\message\MessageAction;
use chat\data\user\User as ChatUser;
use chat\data\user\UserAction as ChatUserAction;
use chat\system\box\RoomListBoxController;
use wcf\data\AbstractDatabaseObjectAction;
use wcf\data\box\Box;
use wcf\data\ISortableAction;
use wcf\data\object\type\ObjectTypeCache;
use wcf\data\package\PackageCache;
use wcf\data\user\UserProfile;
use wcf\system\cache\runtime\UserProfileRuntimeCache;
use wcf\system\database\util\PreparedStatementConditionBuilder;
use wcf\system\event\EventHandler;
use wcf\system\exception\PermissionDeniedException;
use wcf\system\exception\UserInputException;
use wcf\system\push\PushHandler;
use wcf\system\user\activity\point\UserActivityPointHandler;
use wcf\system\WCF;

/**
 * Executes chat room-related actions.
 */
class RoomAction extends AbstractDatabaseObjectAction implements ISortableAction
{
    /**
     * @inheritDoc
     */
    protected $permissionsDelete = [
        'admin.chat.canManageRoom',
    ];

    /**
     * @inheritDoc
     */
    protected $permissionsUpdate = [
        'admin.chat.canManageRoom',
    ];

    /**
     * Validates parameters and permissions.
     */
    public function validateJoin()
    {
        unset($this->parameters['user']);

        $this->readString('sessionID');
        $this->parameters['sessionID'] = \pack(
            'H*',
            \str_replace('-', '', $this->parameters['sessionID'])
        );

        $this->readInteger('roomID');

        $room = RoomCache::getInstance()->getRoom($this->parameters['roomID']);
        if ($room === null) {
            throw new UserInputException('roomID');
        }
        if (!$room->canSee($user = null, $reason)) {
            throw $reason;
        }
        if (!$room->canJoin($user = null, $reason)) {
            throw $reason;
        }
    }

    /**
     * Makes the given user join the current chat room.
     */
    public function join()
    {
        $objectTypeID = ObjectTypeCache::getInstance()->getObjectTypeIDByName(
            'be.bastelstu.chat.messageType',
            'be.bastelstu.chat.messageType.join'
        );
        if (!$objectTypeID) {
            throw new \LogicException('Missing object type');
        }
        // User cannot be set during an AJAX request, but may be set by Tim’s Chat itself.
        if (!isset($this->parameters['user'])) {
            $this->parameters['user'] = WCF::getUser();
        }
        $user = new ChatUser($this->parameters['user']);

        // Check parameters
        $room = RoomCache::getInstance()->getRoom($this->parameters['roomID']);
        if ($room === null) {
            throw new UserInputException('roomID');
        }
        $sessionID = $this->parameters['sessionID'];
        if (\strlen($sessionID) !== 16) {
            throw new UserInputException('sessionID');
        }

        try {
            // Create room_to_user mapping.
            $sql = "INSERT INTO chat1_room_to_user
                                (active, roomID, userID)
                    VALUES      (?, ?, ?)";
            $statement = WCF::getDB()->prepare($sql);
            $statement->execute([ 0, $room->roomID, $user->userID ]);
        } catch (\wcf\system\database\exception\DatabaseException $e) {
            // Ignore if there already is a mapping.
            if ((string)$e->getCode() !== '23000') {
                throw $e;
            }
        }

        try {
            $sql = "INSERT INTO chat1_session
                                (roomID, userID, sessionID, lastRequest)
                    VALUES      (?, ?, ?, ?)";
            $statement = WCF::getDB()->prepare($sql);
            $statement->execute([
                $room->roomID,
                $user->userID,
                $sessionID,
                TIME_NOW,
            ]);
        } catch (\wcf\system\database\exception\DatabaseException $e) {
            if ((string)$e->getCode() !== '23000') {
                throw $e;
            }

            throw new UserInputException('sessionID');
        }

        $markAsBack = static function () use ($user, $room) {
            $userProfile = new UserProfile($user->getDecoratedObject());
            $package = PackageCache::getInstance()->getPackageByIdentifier('be.bastelstu.chat');
            $command = CommandCache::getInstance()->getCommandByPackageAndIdentifier($package, 'back');
            $processor = $command->getProcessor();
            $processor->execute([ ], $room, $userProfile);
        };

        if ($user->chatAway !== null) {
            $markAsBack();
        }

        // Attempt to mark the user as active in the room.
        $sql = "UPDATE  chat1_room_to_user
                SET     active = ?
                WHERE   roomID = ?
                    AND userID = ?";
        $statement = WCF::getDB()->prepare($sql);
        $statement->execute([ 1, $room->roomID, $user->userID ]);
        if ($statement->getAffectedRows() === 0) {
            // The User already is inside the room: Nothing to do here.
            return;
        }

        // Update lastPull. This must not be merged into the above query, because of the 'getAffectedRows' check.
        $sql = "UPDATE  chat1_room_to_user
                SET     lastPull = ?
                WHERE   roomID = ?
                    AND userID = ?";
        $statement = WCF::getDB()->prepare($sql);
        $statement->execute([ TIME_NOW, $room->roomID, $user->userID ]);

        (new MessageAction(
            [ ],
            'create',
            [
                'data' => [
                    'roomID' => $room->roomID,
                    'userID' => $user->userID,
                    'username' => $user->username,
                    'time' => TIME_NOW,
                    'objectTypeID' => $objectTypeID,
                    'payload' => \serialize([ ]),
                ],
            ]
        ))->executeAction();

        UserActivityPointHandler::getInstance()->fireEvent(
            'be.bastelstu.chat.activityPointEvent.join',
            0,
            $user->userID
        );
        $pushHandler = PushHandler::getInstance();
        $pushHandler->sendMessage([
            'message' => 'be.bastelstu.chat.join',
            'target' => 'registered',
        ]);
    }

    /**
     * Validates parameters and permissions.
     */
    public function validateLeave()
    {
        unset($this->parameters['user']);

        $this->readString('sessionID');
        $this->parameters['sessionID'] = \pack(
            'H*',
            \str_replace('-', '', $this->parameters['sessionID'])
        );

        $this->readInteger('roomID');
        $room = RoomCache::getInstance()->getRoom($this->parameters['roomID']);
        if ($room === null) {
            throw new UserInputException('roomID');
        }
        // Do not check permissions: If the user is not inside the room nothing happens, if he is it
        //                           may lead to a faster eviction of the user.
    }

    /**
     * Makes the given user leave the current chat room.
     */
    public function leave()
    {
        $objectTypeID = ObjectTypeCache::getInstance()->getObjectTypeIDByName(
            'be.bastelstu.chat.messageType',
            'be.bastelstu.chat.messageType.leave'
        );
        if ($objectTypeID) {
            // User cannot be set during an AJAX request, but may be set by Tim’s Chat itself.
            if (!isset($this->parameters['user'])) {
                $this->parameters['user'] = WCF::getUser();
            }
            $user = new ChatUser($this->parameters['user']);

            $room = RoomCache::getInstance()->getRoom($this->parameters['roomID']);
            if ($room === null) {
                throw new UserInputException('roomID');
            }

            $sessionID = null;
            if (isset($this->parameters['sessionID'])) {
                $sessionID = $this->parameters['sessionID'];
                if (\strlen($sessionID) !== 16) {
                    throw new UserInputException('sessionID');
                }
            }

            // Delete session.
            $condition = new PreparedStatementConditionBuilder();
            $condition->add('roomID = ?', [ $room->roomID ]);
            $condition->add('userID = ?', [ $user->userID ]);
            if ($sessionID !== null) {
                $condition->add('sessionID = ?', [ $sessionID ]);
            }
            $sql = "DELETE FROM chat1_session
                   {$condition}";
            $statement = WCF::getDB()->prepare($sql);
            $statement->execute($condition->getParameters());
            if ($statement->getAffectedRows() === 0) {
                throw new UserInputException('sessionID');
            }

            try {
                $commited = false;
                WCF::getDB()->beginTransaction();

                // Check whether we deleted the last session.
                $sql = "SELECT  COUNT(*)
                        FROM    chat1_session
                        WHERE   roomID = ?
                            AND userID = ?";
                $statement = WCF::getDB()->prepare($sql);
                $statement->execute([ $room->roomID, $user->userID ]);

                // We did not: Nothing to do here.
                if ($statement->fetchColumn()) {
                    return;
                }

                // Mark the user as inactive.
                $sql = "UPDATE  chat1_room_to_user
                        SET     active = ?
                        WHERE   roomID = ?
                            AND userID = ?";
                $statement = WCF::getDB()->prepare($sql);
                $statement->execute([ 0, $room->roomID, $user->userID ]);
                \assert($statement->getAffectedRows() > 0);

                WCF::getDB()->commitTransaction();
                $commited = true;
            } finally {
                if (!$commited) {
                    WCF::getDB()->rollBackTransaction();
                }
            }

            (new MessageAction(
                [ ],
                'create',
                [
                    'data' => [
                        'roomID' => $room->roomID,
                        'userID' => $user->userID,
                        'username' => $user->username,
                        'time' => TIME_NOW,
                        'objectTypeID' => $objectTypeID,
                        'payload' => \serialize([ ]),
                    ],
                ]
            ))->executeAction();

            $pushHandler = PushHandler::getInstance();
            $pushHandler->sendMessage([
                'message' => 'be.bastelstu.chat.leave',
                'target' => 'registered',
            ]);
        } else {
            throw new \LogicException('Missing object type');
        }
    }

    /**
     * Validates parameters and permissions.
     */
    public function validateGetUsers()
    {
        if (empty($this->getObjects())) {
            $this->readObjects();
        }
        if (\count($this->getObjects()) !== 1) {
            throw new UserInputException('objectIDs');
        }

        $room = $this->getObjects()[0];

        $user = new ChatUser(WCF::getUser());
        if (!$user->isInRoom($room->getDecoratedObject())) {
            throw new PermissionDeniedException();
        }
    }

    /**
     * Returns the userIDs of the users in this room.
     */
    public function getUsers()
    {
        if (empty($this->getObjects())) {
            $this->readObjects();
        }
        if (\count($this->getObjects()) !== 1) {
            throw new UserInputException('objectIDs');
        }

        $room = $this->getObjects()[0];

        $users = (new ChatUserAction(
            [ ],
            'getUsersByID',
            [
                'userIDs' => \array_keys($room->getUsers()),
            ]
        ))->executeAction()['returnValues'];

        $users = \array_map(static function (array $user) use ($room) {
            $userProfile = UserProfileRuntimeCache::getInstance()->getObject($user['userID']);
            if (!isset($user['permissions'])) {
                $user['permissions'] = [];
            }
            $user['permissions']['canWritePublicly'] = $room->canWritePublicly($userProfile);

            return $user;
        }, $users);

        EventHandler::getInstance()->fireAction($this, 'getUsers', $users);

        return $users;
    }

    /**
     * @inheritDoc
     */
    public function validateUpdatePosition()
    {
        // validate permissions
        if (\is_array($this->permissionsUpdate) && !empty($this->permissionsUpdate)) {
            WCF::getSession()->checkPermissions($this->permissionsUpdate);
        } else {
            throw new PermissionDeniedException();
        }

        $this->readIntegerArray('structure', false, 'data');

        $roomList = new RoomList();
        $roomList->readObjects();

        foreach ($this->parameters['data']['structure'][0] as $roomID) {
            $room = $roomList->search($roomID);
            if ($room === null) {
                throw new UserInputException('structure');
            }
        }
    }

    /**
     * @inheritDoc
     */
    public function updatePosition()
    {
        $roomList = new RoomList();
        $roomList->readObjects();

        $i = 0;
        WCF::getDB()->beginTransaction();
        foreach ($this->parameters['data']['structure'][0] as $roomID) {
            $room = $roomList->search($roomID);
            if ($room === null) {
                continue;
            }

            $editor = new RoomEditor($room);
            $editor->update([
                'position' => $i++,
            ]);
        }
        WCF::getDB()->commitTransaction();
    }

    /**
     * Validates permissions.
     */
    public function validateGetBoxRoomList()
    {
        if (!Room::canSeeAny()) {
            throw new PermissionDeniedException();
        }

        $this->readBoolean('isSidebar', true);
        $this->readBoolean('skipEmptyRooms', true);
        $this->readInteger('activeRoomID', true);

        unset($this->parameters['boxController']);
        $this->readInteger('boxID', true);
        if ($this->parameters['boxID']) {
            $box = new Box($this->parameters['boxID']);
            if ($box->boxID) {
                $this->parameters['boxController'] = $box->getController();
                if ($this->parameters['boxController'] instanceof RoomListBoxController) {
                    // all checks passed, end validation; otherwise throw the exception below
                    return;
                }
            }

            throw new UserInputException('boxID');
        }
    }

    /**
     * Returns dashboard roomlist.
     */
    public function getBoxRoomList()
    {
        if (isset($this->parameters['boxController'])) {
            $this->parameters['boxController']->setActiveRoomID($this->parameters['activeRoomID']);

            return [
                'template' => $this->parameters['boxController']->getContent(),
            ];
        }

        // Fetch all rooms, the templates have filtering in place
        $rooms = RoomCache::getInstance()->getRooms();

        $template = 'boxRoomList' . ($this->parameters['isSidebar'] ? 'Sidebar' : '');

        WCF::getTPL()->assign([
            'boxRoomList' => $rooms,
            'skipEmptyRooms' => $this->parameters['skipEmptyRooms'],
            'activeRoomID' => $this->parameters['activeRoomID'],
        ]);

        return [
            'template' => WCF::getTPL()->fetch($template, 'chat'),
        ];
    }
}