<?php
/**
 * 2024 Packlink
 *
 * NOTICE OF LICENSE
 *
 * This source file is subject to the Apache License 2.0
 * that is bundled with this package in the file LICENSE.
 * It is also available through the world-wide-web at this URL:
 * http://www.apache.org/licenses/LICENSE-2.0.txt
 *
 * @author    Packlink <support@packlink.com>
 * @copyright 2024 Packlink Shipping S.L
 * @license   http://www.apache.org/licenses/LICENSE-2.0.txt  Apache License 2.0
 */
namespace Packlink\PrestaShop\Classes\Repositories;

if (!defined('_PS_VERSION_')) {
    exit;
}

use Logeecom\Infrastructure\ORM\Entity;
use Logeecom\Infrastructure\ORM\Interfaces\QueueItemRepository as QueueItemRepositoryInterface;
use Logeecom\Infrastructure\ORM\QueryFilter\Operators;
use Logeecom\Infrastructure\ORM\QueryFilter\QueryFilter;
use Logeecom\Infrastructure\TaskExecution\Exceptions\QueueItemSaveException;
use Logeecom\Infrastructure\TaskExecution\Interfaces\Priority;
use Logeecom\Infrastructure\TaskExecution\QueueItem;

class QueueItemRepository extends BaseRepository implements QueueItemRepositoryInterface
{
    /**
     * Fully qualified name of this class.
     */
    const THIS_CLASS_NAME = __CLASS__;

    /**
     * Finds list of earliest queued queue items per queue. Following list of criteria for searching must be satisfied:
     *      - Queue must be without already running queue items
     *      - For one queue only one (oldest queued) item should be returned
     *
     * @param int $priority Queue item priority.
     * @param int $limit Result set limit. By default max 10 earliest queue items will be returned
     *
     * @return QueueItem[] Found queue item list
     * @throws \Logeecom\Infrastructure\ORM\Exceptions\QueryFilterInvalidParamException
     * @throws \PrestaShopException
     */
    public function findOldestQueuedItems($priority, $limit = 10)
    {
        if ($priority !== Priority::NORMAL) {
            return array();
        }

        $queuedItems = array();

        try {
            $runningQueueNames = $this->getRunningQueueNames();
            $queuedItems = $this->getQueuedItems($runningQueueNames, $limit);
        } catch (\PrestaShopDatabaseException $exception) {
            // In case of database exception return empty result set.
        }

        return $queuedItems;
    }

    /**
     * Creates or updates given queue item. If queue item id is not set, new queue item will be created otherwise
     * update will be performed.
     *
     * @param QueueItem $queueItem Item to save
     * @param array $additionalWhere List of key/value pairs that must be satisfied upon saving queue item. Key is
     *  queue item property and value is condition value for that property. Example for MySql storage:
     *  $storage->save($queueItem, array('status' => 'queued')) should produce query
     *  UPDATE queue_storage_table SET .... WHERE .... AND status => 'queued'
     *
     * @return int Id of saved queue item
     *
     * @throws QueueItemSaveException if queue item could not be saved
     * @throws \Logeecom\Infrastructure\ORM\Exceptions\QueryFilterInvalidParamException
     * @throws \PrestaShopException
     */
    public function saveWithCondition(QueueItem $queueItem, array $additionalWhere = array())
    {
        $savedItemId = null;
        try {
            $itemId = $queueItem->getId();
            if ($itemId === null || $itemId <= 0) {
                $savedItemId = $this->save($queueItem);
            } else {
                $this->updateQueueItem($queueItem, $additionalWhere);
            }
        } catch (\PrestaShopDatabaseException $exception) {
            throw new QueueItemSaveException(
                'Failed to save queue item. SQL error: ' . \Db::getInstance()->getMsgError(),
                0,
                $exception
            );
        }

        return $savedItemId ?: $itemId;
    }

    /**
     * Prepares data for inserting a new record or updating an existing one.
     *
     * @param Entity $entity Packlink entity object.
     * @param array $indexes Array of index values.
     *
     * @return array Prepared record for inserting or updating.
     */
    protected function prepareDataForInsertOrUpdate(Entity $entity, array $indexes)
    {
        // This workaround is necessary because the queue item used to require seven indexes.
        // The core has introduced the eight index for task priority.
        // The migration script that adds column for the eight index is added in version 2.2.4.
        // But update scripts (prior to the version 2.2.4) save queue items. This means that those
        // update scripts will fail (they are trying to save a queue item with eight index columns
        // before the column is created. The priority is not used in the Prestashop integration,
        // therefore we can ignore its column allowing us to save the queue item in prior update
        // scripts.

        $record = array('data' => pSQL($this->serializeEntity($entity), true));

        foreach ($indexes as $index => $value) {
            if ($index > 7) {
                break;
            }

            $record['index_' . $index] = $value !== null ? pSQL($value, true) : null;
        }

        return $record;
    }

    /**
     * Updates queue item.
     *
     * @param QueueItem $queueItem
     * @param array $additionalWhere
     *
     * @throws \Logeecom\Infrastructure\ORM\Exceptions\QueryFilterInvalidParamException
     * @throws \Logeecom\Infrastructure\TaskExecution\Exceptions\QueueItemSaveException
     * @throws \PrestaShopDatabaseException
     * @throws \PrestaShopException
     */
    private function updateQueueItem($queueItem, array $additionalWhere)
    {
        $filter = new QueryFilter();
        $filter->where('id', Operators::EQUALS, $queueItem->getId());

        foreach ($additionalWhere as $name => $value) {
            $filter->where($name, Operators::EQUALS, $value === null ? '' : $value);
        }

        /** @var QueueItem $item */
        $item = $this->selectOne($filter);
        if ($item === null) {
            throw new QueueItemSaveException("Cannot update queue item with id {$queueItem->getId()}.");
        }

        $this->update($queueItem);
    }

    /**
     * Returns names of queues containing items that are currently in progress.
     *
     * @return array Names of queues containing items that are currently in progress.
     *
     * @throws \Logeecom\Infrastructure\ORM\Exceptions\QueryFilterInvalidParamException
     * @throws \PrestaShopDatabaseException
     * @throws \PrestaShopException
     */
    private function getRunningQueueNames()
    {
        $filter = new QueryFilter();
        $filter->where('status', Operators::EQUALS, pSQL(QueueItem::IN_PROGRESS));
        $filter->setLimit(10000);

        /** @var QueueItem[] $runningQueueItems */
        $runningQueueItems = $this->select($filter);

        return array_map(
            function (QueueItem $runningQueueItem) {
                return $runningQueueItem->getQueueName();
            },
            $runningQueueItems
        );
    }

    /**
     * Returns all queued items.
     *
     * @param array $runningQueueNames Array of queues containing items that are currently in progress.
     * @param int $limit Maximum number of records that can be retrieved.
     *
     * @return QueueItem[] Array of queued items.
     *
     * @throws \PrestaShopException
     */
    private function getQueuedItems(array $runningQueueNames, $limit)
    {
        $queuedItems = array();
        $queueNameIndex = $this->getIndexMapping('queueName');

        try {
            $condition = sprintf(
                ' %s',
                $this->buildWhereString(array(
                    'type' => 'QueueItem',
                    $this->getIndexMapping('status') => QueueItem::QUEUED,
                ))
            );

            if (!empty($runningQueueNames)) {
                $condition .= sprintf(
                    ' AND ' . $queueNameIndex . " NOT IN ('%s')",
                    implode("', '", array_map('pSQL', $runningQueueNames))
                );
            }

            $queueNamesQuery = new \DbQuery();
            $queueNamesQuery->select($queueNameIndex . ', MIN(id) AS id')
                ->from(static::TABLE_NAME)
                ->where($condition)
                ->groupBy($queueNameIndex)
                ->limit($limit);

            $query = 'SELECT queueTable.id,queueTable.data'
                . ' FROM (' . $queueNamesQuery->build() . ') AS queueView'
                . ' INNER JOIN ' . bqSQL(_DB_PREFIX_ . static::TABLE_NAME) . ' AS queueTable'
                . ' ON queueView.id = queueTable.id';

            $records = \Db::getInstance()->executeS($query);
            $queuedItems = $this->unserializeEntities($records);
        } catch (\PrestaShopDatabaseException $exception) {
            // In case of exception return empty result set
        }

        return $queuedItems;
    }

    /**
     * Build properly escaped where condition string based on given key/value parameters.
     * String parameters will be sanitized with pSQL method call and other fields will be cast to integer values
     *
     * @param array $whereFields Key value pairs of where condition
     *
     * @return string Properly sanitized where condition string
     */
    private function buildWhereString(array $whereFields = array())
    {
        $where = array();
        foreach ($whereFields as $field => $value) {
            $where[] = bqSQL($field) . Operators::EQUALS . "'" . pSQL($value) . "'";
        }

        return implode(' AND ', $where);
    }
}
