<?php
/**
 * Copyright © Magento, Inc. All rights reserved.
 * See COPYING.txt for license details.
 */
namespace Magento\Review\Model\ResourceModel;

use Magento\Framework\App\Config\ScopeConfigInterface;
use Magento\Framework\App\ObjectManager;

/**
 * Rating resource model
 *
 * @api
 *
 * @author      Magento Core Team <core@magentocommerce.com>
 * @since 100.0.2
 * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
 */
class Rating extends \Magento\Framework\Model\ResourceModel\Db\AbstractDb
{
    const RATING_STATUS_APPROVED = 'Approved';

    /**
     * Store manager
     *
     * @var \Magento\Store\Model\StoreManagerInterface
     */
    protected $_storeManager;

    /**
     * @var \Magento\Framework\Module\Manager
     */
    protected $moduleManager;

    /**
     * @var \Psr\Log\LoggerInterface
     */
    protected $_logger;

    /**
     * @var ScopeConfigInterface
     */
    private $scopeConfig;

    /**
     * @param \Magento\Framework\Model\ResourceModel\Db\Context $context
     * @param \Psr\Log\LoggerInterface $logger
     * @param \Magento\Framework\Module\Manager $moduleManager
     * @param \Magento\Store\Model\StoreManagerInterface $storeManager
     * @param Review\Summary $reviewSummary
     * @param string $connectionName
     * @param ScopeConfigInterface|null $scopeConfig
     */
    public function __construct(
        \Magento\Framework\Model\ResourceModel\Db\Context $context,
        \Psr\Log\LoggerInterface $logger,
        \Magento\Framework\Module\Manager $moduleManager,
        \Magento\Store\Model\StoreManagerInterface $storeManager,
        \Magento\Review\Model\ResourceModel\Review\Summary $reviewSummary,
        $connectionName = null,
        ScopeConfigInterface $scopeConfig = null
    ) {
        $this->moduleManager = $moduleManager;
        $this->_storeManager = $storeManager;
        $this->_logger = $logger;
        $this->_reviewSummary = $reviewSummary;
        $this->scopeConfig = $scopeConfig ?: ObjectManager::getInstance()->get(ScopeConfigInterface::class);
        parent::__construct($context, $connectionName);
    }

    /**
     * Resource initialization
     *
     * @return void
     */
    protected function _construct()
    {
        $this->_init('rating', 'rating_id');
    }

    /**
     * Initialize unique fields
     *
     * @return $this
     */
    protected function _initUniqueFields()
    {
        $this->_uniqueFields = [['field' => 'rating_code', 'title' => '']];
        return $this;
    }

    /**
     * Retrieve select object for load object data
     *
     * @param string $field
     * @param mixed $value
     * @param \Magento\Review\Model\Rating $object
     * @return \Magento\Framework\DB\Select
     */
    protected function _getLoadSelect($field, $value, $object)
    {
        $connection = $this->getConnection();

        $table = $this->getMainTable();
        $storeId = (int)$this->_storeManager->getStore(\Magento\Store\Model\Store::ADMIN_CODE)->getId();
        $select = parent::_getLoadSelect($field, $value, $object);
        $codeExpr = $connection->getIfNullSql('title.value', "{$table}.rating_code");

        $select->joinLeft(
            ['title' => $this->getTable('rating_title')],
            $connection->quoteInto("{$table}.rating_id = title.rating_id AND title.store_id = ?", $storeId),
            ['rating_code' => $codeExpr]
        );

        return $select;
    }

    /**
     * Actions after load
     *
     * @param \Magento\Framework\Model\AbstractModel|\Magento\Review\Model\Rating $object
     * @return $this
     */
    protected function _afterLoad(\Magento\Framework\Model\AbstractModel $object)
    {
        parent::_afterLoad($object);

        if (!$object->getId()) {
            return $this;
        }

        $connection = $this->getConnection();
        $bind = [':rating_id' => (int)$object->getId()];
        // load rating titles
        $select = $connection->select()->from(
            $this->getTable('rating_title'),
            ['store_id', 'value']
        )->where(
            'rating_id=:rating_id'
        );

        $result = $connection->fetchPairs($select, $bind);
        if ($result) {
            $object->setRatingCodes($result);
        }

        // load rating available in stores
        $object->setStores($this->getStores((int)$object->getId()));

        return $this;
    }

    /**
     * Retrieve store IDs related to given rating
     *
     * @param  int $ratingId
     * @return array
     */
    public function getStores($ratingId)
    {
        $select = $this->getConnection()->select()->from(
            $this->getTable('rating_store'),
            'store_id'
        )->where(
            'rating_id = ?',
            $ratingId
        );
        return $this->getConnection()->fetchCol($select);
    }

    /**
     * Actions after save
     *
     * @param \Magento\Framework\Model\AbstractModel|\Magento\Review\Model\Rating $object
     * @return $this
     */
    protected function _afterSave(\Magento\Framework\Model\AbstractModel $object)
    {
        parent::_afterSave($object);
        if ($object->hasRatingCodes()) {
            $this->processRatingCodes($object);
        }

        if ($object->hasStores()) {
            $this->processRatingStores($object);
        }

        return $this;
    }

    /**
     * Process rating codes
     *
     * @param \Magento\Framework\Model\AbstractModel $object
     * @return $this
     */
    protected function processRatingCodes(\Magento\Framework\Model\AbstractModel $object)
    {
        $connection = $this->getConnection();
        $ratingId = (int)$object->getId();
        $table = $this->getTable('rating_title');
        $select = $connection->select()->from($table, ['store_id', 'value'])
            ->where('rating_id = :rating_id');
        $old = $connection->fetchPairs($select, [':rating_id' => $ratingId]);
        $new = array_filter(array_map('trim', $object->getRatingCodes()));
        $this->deleteRatingData($ratingId, $table, array_keys(array_diff_assoc($old, $new)));

        $insert = [];
        foreach (array_diff_assoc($new, $old) as $storeId => $title) {
            $insert[] = ['rating_id' => $ratingId, 'store_id' => (int)$storeId, 'value' => $title];
        }
        $this->insertRatingData($table, $insert);
        return $this;
    }

    /**
     * Process rating stores
     *
     * @param \Magento\Framework\Model\AbstractModel $object
     * @return $this
     */
    protected function processRatingStores(\Magento\Framework\Model\AbstractModel $object)
    {
        $connection = $this->getConnection();
        $ratingId = (int)$object->getId();
        $table = $this->getTable('rating_store');
        $select = $connection->select()->from($table, ['store_id'])
            ->where('rating_id = :rating_id');
        $old = $connection->fetchCol($select, [':rating_id' => $ratingId]);
        $new = $object->getStores();
        $this->deleteRatingData($ratingId, $table, array_diff($old, $new));

        $insert = [];
        foreach (array_diff($new, $old) as $storeId) {
            $insert[] = ['rating_id' => $ratingId, 'store_id' => (int)$storeId];
        }
        $this->insertRatingData($table, $insert);
        return $this;
    }

    /**
     * Delete rating data
     *
     * @param int $ratingId
     * @param string $table
     * @param array $storeIds
     * @return void
     */
    protected function deleteRatingData($ratingId, $table, array $storeIds)
    {
        if (empty($storeIds)) {
            return;
        }
        $connection = $this->getConnection();
        $connection->beginTransaction();
        try {
            $where = ['rating_id = ?' => $ratingId, 'store_id IN(?)' => $storeIds];
            $connection->delete($table, $where);
            $connection->commit();
        } catch (\Exception $e) {
            $this->_logger->critical($e);
            $connection->rollBack();
        }
    }

    /**
     * Insert rating data
     *
     * @param string $table
     * @param array $data
     * @return void
     */
    protected function insertRatingData($table, array $data)
    {
        if (empty($data)) {
            return;
        }
        $connection = $this->getConnection();
        $connection->beginTransaction();
        try {
            $connection->insertMultiple($table, $data);
            $connection->commit();
        } catch (\Exception $e) {
            $this->_logger->critical($e);
            $connection->rollBack();
        }
    }

    /**
     * Perform actions after object delete
     *
     * Prepare rating data for reaggregate all data for reviews
     *
     * @param \Magento\Framework\Model\AbstractModel $object
     * @return $this
     */
    protected function _afterDelete(\Magento\Framework\Model\AbstractModel $object)
    {
        parent::_afterDelete($object);
        if (!$this->moduleManager->isEnabled('Magento_Review') &&
            !$this->scopeConfig->getValue(
                \Magento\Review\Observer\PredispatchReviewObserver::XML_PATH_REVIEW_ACTIVE,
                \Magento\Store\Model\ScopeInterface::SCOPE_STORE
            )
        ) {
            return $this;
        }
        $data = $this->_getEntitySummaryData($object);
        $summary = [];
        foreach ($data as $row) {
            $clone = clone $object;
            $clone->addData($row);
            $summary[$clone->getStoreId()][$clone->getEntityPkValue()] = $clone;
        }
        $this->_reviewSummary->reAggregate($summary);
        return $this;
    }

    /**
     * Return array of rating summary
     *
     * @param \Magento\Review\Model\Rating $object
     * @param boolean $onlyForCurrentStore
     * @return array
     */
    public function getEntitySummary($object, $onlyForCurrentStore = true)
    {
        $data = $this->_getEntitySummaryData($object);

        if ($onlyForCurrentStore) {
            foreach ($data as $row) {
                if ($row['store_id'] == $this->_storeManager->getStore()->getId()) {
                    $object->addData($row);
                }
            }
            return $object;
        }

        $stores = $this->_storeManager->getStores();

        $result = [];
        foreach ($data as $row) {
            $clone = clone $object;
            $clone->addData($row);
            $result[$clone->getStoreId()] = $clone;
        }

        $usedStoresId = array_keys($result);
        foreach ($stores as $store) {
            if (!in_array($store->getId(), $usedStoresId)) {
                $clone = clone $object;
                $clone->setCount(0);
                $clone->setSum(0);
                $clone->setStoreId($store->getId());
                $result[$store->getId()] = $clone;
            }
        }
        return array_values($result);
    }

    /**
     * Return data of rating summary
     *
     * @param \Magento\Review\Model\Rating $object
     * @return array
     */
    protected function _getEntitySummaryData($object)
    {
        $connection = $this->getConnection();

        $sumColumn = new \Zend_Db_Expr("SUM(rating_vote.{$connection->quoteIdentifier('percent')})");
        $countColumn = new \Zend_Db_Expr("COUNT(*)");

        $select = $connection->select()->from(
            ['rating_vote' => $this->getTable('rating_option_vote')],
            ['entity_pk_value' => 'rating_vote.entity_pk_value', 'sum' => $sumColumn, 'count' => $countColumn]
        )->join(
            ['review' => $this->getTable('review')],
            'rating_vote.review_id=review.review_id',
            []
        )->joinLeft(
            ['review_store' => $this->getTable('review_store')],
            'rating_vote.review_id=review_store.review_id',
            ['review_store.store_id']
        );
        if (!$this->_storeManager->isSingleStoreMode()) {
            $select->join(
                ['rating_store' => $this->getTable('rating_store')],
                'rating_store.rating_id = rating_vote.rating_id AND rating_store.store_id = review_store.store_id',
                []
            );
        }
        $select->join(
            ['review_status' => $this->getTable('review_status')],
            'review.status_id = review_status.status_id',
            []
        )->where(
            'review_status.status_code = :status_code'
        )->group(
            'rating_vote.entity_pk_value'
        )->group(
            'review_store.store_id'
        );
        $bind = [':status_code' => self::RATING_STATUS_APPROVED];

        $entityPkValue = $object->getEntityPkValue();
        if ($entityPkValue) {
            $select->where('rating_vote.entity_pk_value = :pk_value');
            $bind[':pk_value'] = $entityPkValue;
        }

        return $connection->fetchAll($select, $bind);
    }

    /**
     * Review summary
     *
     * @param \Magento\Review\Model\Rating $object
     * @param boolean $onlyForCurrentStore
     * @return array
     */
    public function getReviewSummary($object, $onlyForCurrentStore = true)
    {
        $connection = $this->getConnection();

        $sumColumn = new \Zend_Db_Expr("SUM(rating_vote.{$connection->quoteIdentifier('percent')})");
        $countColumn = new \Zend_Db_Expr('COUNT(*)');
        $select = $connection->select()->from(
            ['rating_vote' => $this->getTable('rating_option_vote')],
            ['sum' => $sumColumn, 'count' => $countColumn]
        )->joinLeft(
            ['review_store' => $this->getTable('review_store')],
            'rating_vote.review_id = review_store.review_id',
            ['review_store.store_id']
        );
        if (!$this->_storeManager->isSingleStoreMode()) {
            $select->join(
                ['rating_store' => $this->getTable('rating_store')],
                'rating_store.rating_id = rating_vote.rating_id AND rating_store.store_id = review_store.store_id',
                []
            );
        }
        $select->where(
            'rating_vote.review_id = :review_id'
        )->group(
            'rating_vote.review_id'
        )->group(
            'review_store.store_id'
        );

        $data = $connection->fetchAll($select, [':review_id' => $object->getReviewId()]);

        $currentStore = $this->_storeManager->isSingleStoreMode() ? $this->_storeManager->getStore()->getId() : null;

        if ($onlyForCurrentStore) {
            foreach ($data as $row) {
                if ($row['store_id'] !== $currentStore) {
                    $object->addData($row);
                }
            }
            return $object;
        }

        $result = [];

        $stores = $this->_storeManager->getStore()->getResourceCollection()->load();

        foreach ($data as $row) {
            $clone = clone $object;
            $clone->addData($row);
            $result[$clone->getStoreId()] = $clone;
        }

        $usedStoresId = array_keys($result);

        foreach ($stores as $store) {
            if (!in_array($store->getId(), $usedStoresId)) {
                $clone = clone $object;
                $clone->setCount(0);
                $clone->setSum(0);
                $clone->setStoreId($store->getId());
                $result[$store->getId()] = $clone;
            }
        }

        return array_values($result);
    }

    /**
     * Get rating entity type id by code
     *
     * @param string $entityCode
     * @return int
     */
    public function getEntityIdByCode($entityCode)
    {
        $select = $this->getConnection()->select()->from(
            $this->getTable('rating_entity'),
            ['entity_id']
        )->where(
            'entity_code = :entity_code'
        );

        return $this->getConnection()->fetchOne($select, [':entity_code' => $entityCode]);
    }

    /**
     * Delete ratings by product id
     *
     * @param int $productId
     * @return $this
     */
    public function deleteAggregatedRatingsByProductId($productId)
    {
        $entityId = $this->getEntityIdByCode(\Magento\Review\Model\Rating::ENTITY_PRODUCT_CODE);
        $connection = $this->getConnection();
        $select = $connection->select()->from($this->getMainTable(), 'rating_id')->where('entity_id = :entity_id');
        $ratingIds = $connection->fetchCol($select, [':entity_id' => $entityId]);

        if ($ratingIds) {
            $where = ['entity_pk_value = ?' => (int)$productId, 'rating_id IN(?)' => $ratingIds];
            $connection->delete($this->getTable('rating_option_vote_aggregated'), $where);
        }

        return $this;
    }
}