<?php

/**
 * @file classes/query/QueryDAO.php
 *
 * Copyright (c) 2016-2021 Simon Fraser University
 * Copyright (c) 2000-2021 John Willinsky
 * Distributed under the GNU GPL v3. For full terms see the file docs/COPYING.
 *
 * @class QueryDAO
 *
 * @ingroup query
 *
 * @see Query
 *
 * @brief Operations for retrieving and modifying Query objects.
 */

namespace PKP\query;

use APP\core\Application;
use APP\facades\Repo;
use APP\notification\Notification;
use APP\notification\NotificationManager;
use APP\submission\Submission;
use Illuminate\Database\Query\Builder;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Mail;
use PKP\core\Core;
use PKP\db\DAORegistry;
use PKP\db\DAOResultFactory;
use PKP\mail\Mailable;
use PKP\note\NoteDAO;
use PKP\notification\NotificationDAO;
use PKP\notification\NotificationSubscriptionSettingsDAO;
use PKP\notification\PKPNotification;
use PKP\plugins\Hook;
use PKP\security\Role;
use PKP\stageAssignment\StageAssignment;
use PKP\stageAssignment\StageAssignmentDAO;
use PKP\user\User;

class QueryDAO extends \PKP\db\DAO
{
    /**
     * Retrieve a submission query by ID.
     *
     * @param int $queryId Query ID
     * @param int $assocType Optional Application::ASSOC_TYPE_...
     * @param int $assocId Optional assoc ID per assocType
     *
     * @return Query
     */
    public function getById($queryId, $assocType = null, $assocId = null)
    {
        $params = [(int) $queryId];
        if ($assocType) {
            $params[] = (int) $assocType;
            $params[] = (int) $assocId;
        }
        $result = $this->retrieve(
            'SELECT *
			FROM	queries
			WHERE	query_id = ?'
                . ($assocType ? ' AND assoc_type = ? AND assoc_id = ?' : ''),
            $params
        );
        $row = $result->current();
        return $row ? $this->_fromRow((array) $row) : null;
    }

    /**
     * Retrieve all queries by association
     *
     * @param int $assocType Application::ASSOC_TYPE_...
     * @param int $assocId Assoc ID
     * @param int $stageId Optional stage ID
     * @param int $userId Optional user ID; when set, show only assigned queries
     *
     * @return DAOResultFactory<Query> Query
     */
    public function getByAssoc($assocType, $assocId, $stageId = null, $userId = null)
    {
        $params = [];
        $params[] = (int) Application::ASSOC_TYPE_QUERY;
        if ($userId) {
            $params[] = (int) $userId;
        }
        $params[] = (int) $assocType;
        $params[] = (int) $assocId;
        if ($stageId) {
            $params[] = (int) $stageId;
        }
        if ($userId) {
            $params[] = (int) $userId;
        }

        return new DAOResultFactory(
            $this->retrieve(
                'SELECT	DISTINCT q.*
				FROM	queries q
				LEFT JOIN notes n ON n.assoc_type = ? AND n.assoc_id = q.query_id
				' . ($userId ? 'INNER JOIN query_participants qp ON (q.query_id = qp.query_id AND qp.user_id = ?)' : '') . '
				WHERE	q.assoc_type = ? AND q.assoc_id = ?
				' . ($stageId ? ' AND q.stage_id = ?' : '') .
                ($userId ? '
				AND (n.user_id = ? OR n.title IS NOT NULL
				OR n.contents IS NOT NULL)' : '') . '
				ORDER BY q.seq',
                $params
            ),
            $this,
            '_fromRow'
        );
    }

    /**
     * Retrieve a count of all open queries totalled by stage
     *
     * @param int[] $participantIds Only include queries with these participants
     *
     * @return array<int,int> [int $stageId => int $count]
     */
    public function countOpenPerStage(int $submissionId, ?array $participantIds = null)
    {
        $counts = DB::table('queries as q')
            ->when($participantIds !== null, function (Builder $q) use ($participantIds) {
                $q->join('query_participants as qp', 'q.query_id', '=', 'qp.query_id')
                    ->whereIn('qp.user_id', $participantIds);
            })
            ->where('q.assoc_type', '=', Application::ASSOC_TYPE_SUBMISSION)
            ->where('q.assoc_id', '=', $submissionId)
            ->where('q.closed', '=', 0)
            ->select(['q.stage_id', DB::raw('COUNT(q.stage_id) as count')])
            ->groupBy(['q.stage_id'])
            ->get()
            ->mapWithKeys(fn ($row, $key) => [$row->stage_id => $row->count])
            ->toArray();

        return collect(Application::get()->getApplicationStages())
            ->mapWithKeys(fn ($stageId, $key) => [$stageId => $counts[$stageId] ?? 0]);
    }

    /**
     * Internal function to return a submission query object from a row.
     *
     * @param array $row
     *
     * @return Query
     */
    public function _fromRow($row)
    {
        $query = $this->newDataObject();
        $query->setId($row['query_id']);
        $query->setAssocType($row['assoc_type']);
        $query->setAssocId($row['assoc_id']);
        $query->setStageId($row['stage_id']);
        $query->setIsClosed($row['closed']);
        $query->setSequence($row['seq']);

        Hook::call('QueryDAO::_fromRow', [&$query, &$row]);
        return $query;
    }

    /**
     * Get a new data object
     *
     * @return Query
     */
    public function newDataObject()
    {
        return new Query();
    }

    /**
     * Insert a new Query.
     *
     * @param Query $query
     *
     * @return int New query ID
     */
    public function insertObject($query)
    {
        $this->update(
            'INSERT INTO queries (assoc_type, assoc_id, stage_id, closed, seq)
			VALUES (?, ?, ?, ?, ?)',
            [
                (int) $query->getAssocType(),
                (int) $query->getAssocId(),
                (int) $query->getStageId(),
                (int) $query->getIsClosed(),
                (float) $query->getSequence(),
            ]
        );
        $query->setId($this->getInsertId());
        return $query->getId();
    }

    /**
     * Adds a participant to a query.
     *
     * @param int $queryId Query ID
     * @param int $userId User ID
     */
    public function insertParticipant($queryId, $userId)
    {
        $this->update(
            'INSERT INTO query_participants
			(query_id, user_id)
			VALUES
			(?, ?)',
            [(int) $queryId, (int) $userId]
        );
    }

    /**
     * Removes a participant from a query.
     *
     * @param int $queryId Query ID
     * @param int $userId User ID
     */
    public function removeParticipant($queryId, $userId)
    {
        $this->update(
            'DELETE FROM query_participants WHERE query_id = ? AND user_id = ?',
            [(int) $queryId, (int) $userId]
        );
    }

    /**
     * Removes all participants from a query.
     *
     * @param int $queryId Query ID
     */
    public function removeAllParticipants($queryId)
    {
        $this->update(
            'DELETE FROM query_participants WHERE query_id = ?',
            [(int) $queryId]
        );
    }

    /**
     * Retrieve all participant user IDs for a query.
     *
     * @param int $queryId Query ID
     * @param int $userId User ID to restrict results to
     *
     * @return array
     */
    public function getParticipantIds($queryId, $userId = null)
    {
        $params = [(int) $queryId];
        if ($userId) {
            $params[] = (int) $userId;
        }
        $result = $this->retrieve(
            'SELECT	user_id
			FROM	query_participants
			WHERE	query_id = ?' .
            ($userId ? ' AND user_id = ?' : ''),
            $params
        );
        $userIds = [];
        foreach ($result as $row) {
            $userIds[] = (int) $row->user_id;
        }
        return $userIds;
    }

    /**
     * Update an existing Query.
     *
     * @param Query $query
     */
    public function updateObject($query)
    {
        $this->update(
            'UPDATE	queries
			SET	assoc_type = ?,
				assoc_id = ?,
				stage_id = ?,
				closed = ?,
				seq = ?
			WHERE	query_id = ?',
            [
                (int) $query->getAssocType(),
                (int) $query->getAssocId(),
                (int) $query->getStageId(),
                (int) $query->getIsClosed(),
                (float) $query->getSequence(),
                (int) $query->getId()
            ]
        );
    }

    /**
     * Delete a submission query.
     *
     * @param Query $query
     */
    public function deleteObject($query)
    {
        $this->deleteById($query->getId());
    }

    /**
     * Delete a submission query by ID.
     *
     * Deletes any associated notes and notifications. The
     * participants will be deleted automatically through
     * the onDelete CASCADE foreign key relationship in
     * the database table.
     *
     * @param int $queryId Query ID
     * @param int $assocType Optional Application::ASSOC_TYPE_...
     * @param int $assocId Optional assoc ID per assocType
     */
    public function deleteById($queryId, $assocType = null, $assocId = null)
    {
        $countDeleted = DB::table('queries')
            ->where('query_id', '=', $queryId)
            ->when(!is_null($assocType), function (Builder $q) use ($assocType) {
                $q->where('assoc_type', '=', $assocType);
            })
            ->when(!is_null($assocId), function (Builder $q) use ($assocId) {
                $q->where('assoc_id', '=', $assocId);
            })
            ->delete();

        if ($countDeleted) {
            $noteDao = DAORegistry::getDAO('NoteDAO'); /** @var NoteDAO $noteDao */
            $noteDao->deleteByAssoc(Application::ASSOC_TYPE_QUERY, $queryId);

            $notificationDao = DAORegistry::getDAO('NotificationDAO'); /** @var NotificationDAO $notificationDao */
            $notifications = $notificationDao->getByAssoc(Application::ASSOC_TYPE_QUERY, $queryId);
            while ($notification = $notifications->next()) {
                $notificationDao->deleteObject($notification);
            }
        }
    }

    /**
     * Sequentially renumber queries in their sequence order.
     *
     * @param int $assocType Application::ASSOC_TYPE_...
     * @param int $assocId Assoc ID per assocType
     */
    public function resequence($assocType, $assocId)
    {
        $result = $this->retrieve(
            'SELECT query_id FROM queries WHERE assoc_type = ? AND assoc_id = ? ORDER BY seq',
            [(int) $assocType, (int) $assocId]
        );

        for ($i = 1; $row = $result->current(); $i++) {
            $this->update('UPDATE queries SET seq = ? WHERE query_id = ?', [$i, $row->query_id]);
            $result->next();
        }
    }

    /**
     * Delete queries by assoc info.
     *
     * @param int $assocType Application::ASSOC_TYPE_...
     * @param int $assocId Assoc ID per assocType
     */
    public function deleteByAssoc($assocType, $assocId)
    {
        $queries = $this->getByAssoc($assocType, $assocId);
        while ($query = $queries->next()) {
            $this->deleteObject($query);
        }
    }

    /**
     * Start a query
     *
     * Inserts the query, assigns participants, and creates the head note
     *
     * @return int The new query id
     */
    public function addQuery(int $submissionId, int $stageId, string $title, string $content, User $fromUser, array $participantUserIds, int $contextId, bool $sendEmail = true): int
    {
        $query = $this->newDataObject();
        $query->setAssocType(Application::ASSOC_TYPE_SUBMISSION);
        $query->setAssocId($submissionId);
        $query->setStageId($stageId);
        $query->setSequence(REALLY_BIG_NUMBER);
        $this->insertObject($query);
        $this->resequence(Application::ASSOC_TYPE_SUBMISSION, $submissionId);

        foreach (array_unique($participantUserIds) as $participantUserId) {
            $this->insertParticipant($query->getId(), $participantUserId);
        }

        $noteDao = DAORegistry::getDAO('NoteDAO'); /** @var NoteDAO $noteDao */
        $note = $noteDao->newDataObject();
        $note->setAssocType(Application::ASSOC_TYPE_QUERY);
        $note->setAssocId($query->getId());
        $note->setContents($content);
        $note->setTitle($title);
        $note->setDateCreated(Core::getCurrentDate());
        $note->setDateModified(Core::getCurrentDate());
        $note->setUserId($fromUser->getId());
        $noteDao->insertObject($note);

        // Add task for assigned participants
        $notificationMgr = new NotificationManager();

        /** @var NotificationSubscriptionSettingsDAO */
        $notificationSubscriptionSettingsDao = DAORegistry::getDAO('NotificationSubscriptionSettingsDAO');

        foreach ($participantUserIds as $participantUserId) {
            $notificationMgr->createNotification(
                Application::get()->getRequest(),
                $participantUserId,
                Notification::NOTIFICATION_TYPE_NEW_QUERY,
                $contextId,
                Application::ASSOC_TYPE_QUERY,
                $query->getId(),
                Notification::NOTIFICATION_LEVEL_TASK
            );

            if (!$sendEmail) {
                continue;
            }

            // Check if the user is unsubscribed
            $notificationSubscriptionSettings = $notificationSubscriptionSettingsDao->getNotificationSubscriptionSettings(
                NotificationSubscriptionSettingsDAO::BLOCKED_EMAIL_NOTIFICATION_KEY,
                $participantUserId,
                $contextId
            );
            if (in_array(PKPNotification::NOTIFICATION_TYPE_NEW_QUERY, $notificationSubscriptionSettings)) {
                continue;
            }

            $recipient = Repo::user()->get($participantUserId);
            $mailable = new Mailable();
            $mailable->to($recipient->getEmail(), $recipient->getFullName());
            $mailable->from($fromUser->getEmail(), $fromUser->getFullName());
            $mailable->subject($title);
            $mailable->body($content);

            Mail::send($mailable);
        }

        return $query->getId();
    }

    /**
     * Create a query with a submission's comments for the editors
     *
     * Creates the query and assigns all participants
     *
     * @return int new query id
     */
    public function addCommentsForEditorsQuery(Submission $submission): int
    {
        /** @var StageAssignmentDAO $stageAssignmentDao */
        $stageAssignmentDao = DAORegistry::getDAO('StageAssignmentDAO');
        $assigned = $stageAssignmentDao->getBySubmissionAndRoleIds(
            $submission->getId(),
            [
                Role::ROLE_ID_MANAGER,
                Role::ROLE_ID_SUB_EDITOR,
                Role::ROLE_ID_ASSISTANT,
                Role::ROLE_ID_AUTHOR,
            ],
            $submission->getData('stageId')
        );
        $assigned = collect($assigned->toArray());

        $participantUserIds = $assigned->map(fn (StageAssignment $stageAssignment) => $stageAssignment->getUserId());

        $authorAssignments = $stageAssignmentDao->getBySubmissionAndRoleIds(
            $submission->getId(),
            [Role::ROLE_ID_AUTHOR],
            $submission->getData('stageId')
        )->toArray();
        $fromUser = empty($authorAssignments)
            ? Application::get()->getRequest()->getUser()
            : Repo::user()->get($authorAssignments[0]->getUserId());

        return $this->addQuery(
            $submission->getId(),
            $submission->getData('stageId'),
            __('submission.submit.coverNote'),
            $submission->getData('commentsForTheEditors'),
            $fromUser,
            $participantUserIds->toArray(),
            $submission->getData('contextId')
        );
    }
}

if (!PKP_STRICT_MODE) {
    class_alias('\PKP\query\QueryDAO', '\QueryDAO');
}
