<?php
/**
 * For the full copyright and license information, please view the
 * docs/licenses/LICENSE.txt file that was distributed with this source code.
 */

declare(strict_types=1);

namespace PrestaShop\PrestaShop\Adapter\Product\Update;

use PrestaShop\PrestaShop\Adapter\Category\Repository\CategoryRepository;
use PrestaShop\PrestaShop\Adapter\Product\Combination\Repository\CombinationRepository;
use PrestaShop\PrestaShop\Adapter\Product\Combination\Update\CombinationStockProperties;
use PrestaShop\PrestaShop\Adapter\Product\Combination\Update\CombinationStockUpdater;
use PrestaShop\PrestaShop\Adapter\Product\Combination\Update\DefaultCombinationUpdater;
use PrestaShop\PrestaShop\Adapter\Product\Image\Repository\ProductImageRepository;
use PrestaShop\PrestaShop\Adapter\Product\Repository\ProductRepository;
use PrestaShop\PrestaShop\Adapter\Product\Stock\Repository\StockAvailableRepository;
use PrestaShop\PrestaShop\Adapter\Product\Stock\Update\ProductStockProperties;
use PrestaShop\PrestaShop\Adapter\Product\Stock\Update\ProductStockUpdater;
use PrestaShop\PrestaShop\Adapter\Shop\Repository\ShopRepository;
use PrestaShop\PrestaShop\Core\Domain\Carrier\ValueObject\CarrierReferenceId;
use PrestaShop\PrestaShop\Core\Domain\Category\ValueObject\CategoryId;
use PrestaShop\PrestaShop\Core\Domain\Product\Exception\CannotUpdateProductException;
use PrestaShop\PrestaShop\Core\Domain\Product\Image\ValueObject\ImageId;
use PrestaShop\PrestaShop\Core\Domain\Product\Stock\Exception\StockAvailableNotFoundException;
use PrestaShop\PrestaShop\Core\Domain\Product\Stock\ValueObject\OutOfStockType;
use PrestaShop\PrestaShop\Core\Domain\Product\Stock\ValueObject\StockModification;
use PrestaShop\PrestaShop\Core\Domain\Product\ValueObject\ProductId;
use PrestaShop\PrestaShop\Core\Domain\Product\ValueObject\ProductType;
use PrestaShop\PrestaShop\Core\Domain\Shop\ValueObject\ShopConstraint;
use PrestaShop\PrestaShop\Core\Domain\Shop\ValueObject\ShopId;
use Product;

/**
 * Class ProductShopUpdater
 */
class ProductShopUpdater
{
    /**
     * @var ProductRepository
     */
    private $productRepository;

    /**
     * @var StockAvailableRepository
     */
    private $stockAvailableRepository;

    /**
     * @var ShopRepository
     */
    private $shopRepository;

    /**
     * @var ProductImageRepository
     */
    private $productImageRepository;

    /**
     * @var ProductStockUpdater
     */
    private $productStockUpdater;

    /**
     * @var CombinationRepository
     */
    private $combinationRepository;

    /**
     * @var CombinationStockUpdater
     */
    private $combinationStockUpdater;

    /**
     * @var DefaultCombinationUpdater
     */
    private $defaultCombinationUpdater;

    /**
     * @var ProductCategoryUpdater
     */
    private $productCategoryUpdater;

    /**
     * @var CategoryRepository
     */
    private $categoryRepository;

    public function __construct(
        ProductRepository $productRepository,
        StockAvailableRepository $stockAvailableRepository,
        ShopRepository $shopRepository,
        ProductImageRepository $productImageRepository,
        ProductStockUpdater $productStockUpdater,
        CombinationRepository $combinationMultiShopRepository,
        CombinationStockUpdater $combinationStockUpdater,
        DefaultCombinationUpdater $defaultCombinationUpdater,
        ProductCategoryUpdater $productCategoryUpdater,
        CategoryRepository $categoryRepository
    ) {
        $this->productRepository = $productRepository;
        $this->stockAvailableRepository = $stockAvailableRepository;
        $this->shopRepository = $shopRepository;
        $this->productImageRepository = $productImageRepository;
        $this->productStockUpdater = $productStockUpdater;
        $this->combinationRepository = $combinationMultiShopRepository;
        $this->combinationStockUpdater = $combinationStockUpdater;
        $this->defaultCombinationUpdater = $defaultCombinationUpdater;
        $this->productCategoryUpdater = $productCategoryUpdater;
        $this->categoryRepository = $categoryRepository;
    }

    /**
     * @param ProductId $productId
     * @param ShopId $sourceShopId
     * @param ShopId $targetShopId
     */
    public function copyToShop(ProductId $productId, ShopId $sourceShopId, ShopId $targetShopId): void
    {
        $this->shopRepository->assertShopExists($sourceShopId);
        $this->shopRepository->assertShopExists($targetShopId);

        /** @var Product $sourceProduct */
        $sourceProduct = $this->productRepository->getByShopConstraint($productId, ShopConstraint::shop($sourceShopId->getValue()));
        $this->productRepository->update(
            $sourceProduct,
            ShopConstraint::shop($targetShopId->getValue()),
            CannotUpdateProductException::FAILED_SHOP_COPY
        );

        // First copy combinations so that stock can be copied after
        $this->copyCategories($productId, $sourceShopId, $targetShopId, $sourceProduct);
        $this->copyCombinations($productId, $sourceShopId, $targetShopId);
        $this->copyStockToShop($productId, $sourceShopId, $targetShopId, $sourceProduct->getProductType());
        $this->copyCarriersToShop($sourceProduct, $targetShopId);
        $this->copyImageAssociations($productId, $sourceShopId, $targetShopId);
    }

    private function copyCategories(ProductId $productId, ShopId $sourceShopId, ShopId $targetShopId, Product $sourceProduct): void
    {
        $productCategories = $this->categoryRepository->getProductCategoryIds($productId, ShopConstraint::shop($sourceShopId->getValue()));
        $this->productCategoryUpdater->updateCategories(
            $productId,
            $productCategories,
            new CategoryId((int) $sourceProduct->id_category_default),
            ShopConstraint::shop($targetShopId->getValue())
        );
    }

    private function copyStockToShop(ProductId $productId, ShopId $sourceShopId, ShopId $targetShopId, string $productType): void
    {
        // First get the source stock
        $sourceStock = $this->stockAvailableRepository->getForProduct($productId, $sourceShopId);
        $outOfStock = new OutOfStockType((int) $sourceStock->out_of_stock);

        // Then try to get the target stock, if it doesn't exist create it
        try {
            $targetStock = $this->stockAvailableRepository->getForProduct($productId, $targetShopId);
        } catch (StockAvailableNotFoundException) {
            $targetStock = $this->stockAvailableRepository->createStockAvailable($productId, $targetShopId);
        }

        $deltaQuantity = (int) $sourceStock->quantity - (int) $targetStock->quantity;
        if ($deltaQuantity !== 0 || (int) $sourceStock->out_of_stock !== (int) $targetStock->out_of_stock || $sourceStock->location !== $targetStock->location) {
            $stockModification = StockModification::buildFixedQuantity((int) $sourceStock->quantity);
            $stockProperties = new ProductStockProperties(
                $stockModification,
                $outOfStock,
                $sourceStock->location
            );
            $this->productStockUpdater->update($productId, $stockProperties, ShopConstraint::shop($targetShopId->getValue()));
        }

        if ($productType === ProductType::TYPE_COMBINATIONS) {
            $this->copyCombinationsStockToShop($productId, $sourceShopId, $targetShopId, $outOfStock);
        }
    }

    private function copyCombinationsStockToShop(ProductId $productId, ShopId $sourceShopId, ShopId $targetShopId, OutOfStockType $outOfStockType): void
    {
        $sourceCombinations = $this->combinationRepository->getCombinationIds(
            $productId,
            ShopConstraint::shop($targetShopId->getValue())
        );
        $targetConstraint = ShopConstraint::shop($targetShopId->getValue());

        foreach ($sourceCombinations as $combinationId) {
            // First get the source stock
            $sourceStock = $this->stockAvailableRepository->getForCombination($combinationId, $sourceShopId);

            // Then try to get the target stock, if it doesn't exist create it
            try {
                $targetStock = $this->stockAvailableRepository->getForCombination($combinationId, $targetShopId);
            } catch (StockAvailableNotFoundException) {
                $targetStock = $this->stockAvailableRepository->createStockAvailable($productId, $targetShopId, $combinationId);
            }

            $deltaQuantity = (int) $sourceStock->quantity - (int) $targetStock->quantity;
            if ($deltaQuantity !== 0) {
                $stockModification = StockModification::buildDeltaQuantity($deltaQuantity);
                $stockProperties = new CombinationStockProperties(
                    $stockModification,
                    $sourceStock->location
                );
                $this->combinationStockUpdater->update($combinationId, $stockProperties, $targetConstraint);
            }
        }
        $this->combinationRepository->updateCombinationOutOfStockType($productId, $outOfStockType, $targetConstraint);
    }

    /**
     * @param Product $sourceProduct
     * @param ShopId $targetShopId
     */
    private function copyCarriersToShop(Product $sourceProduct, ShopId $targetShopId): void
    {
        $sourceCarriers = $sourceProduct->getCarriers();
        $sourceCarrierReferences = [];
        foreach ($sourceCarriers as $sourceCarrier) {
            $sourceCarrierReferences[] = new CarrierReferenceId((int) $sourceCarrier['id_reference']);
        }

        $this->productRepository->setCarrierReferences(
            new ProductId($sourceProduct->id),
            $sourceCarrierReferences,
            ShopConstraint::shop($targetShopId->getValue())
        );
    }

    /**
     * @param ProductId $productId
     * @param ShopId $sourceShopId
     * @param ShopId $targetShopId
     *
     * @return void
     *
     * @throws \PrestaShop\PrestaShop\Core\Domain\Shop\Exception\ShopException
     * @throws \PrestaShop\PrestaShop\Core\Exception\CoreException
     */
    private function copyImageAssociations(ProductId $productId, ShopId $sourceShopId, ShopId $targetShopId): void
    {
        $imagesFromSourceShop = $this->productImageRepository->getImages($productId, ShopConstraint::shop($sourceShopId->getValue()));
        $targetImageIds = array_map(static function (ImageId $imageId): int {
            return $imageId->getValue();
        }, $this->productImageRepository->getImageIds($productId, ShopConstraint::shop($targetShopId->getValue())));

        foreach ($imagesFromSourceShop as $image) {
            // skip image if it is already associated with the target shop
            if (in_array((int) $image->id, $targetImageIds, true)) {
                continue;
            }
            $this->productImageRepository->associateImageToShop($image, $targetShopId);
        }
    }

    private function copyCombinations(ProductId $productId, ShopId $sourceShopId, ShopId $targetShopId): void
    {
        $shopCombinationIds = $this->combinationRepository->getCombinationIds(
            $productId,
            ShopConstraint::shop($sourceShopId->getValue())
        );
        if (empty($shopCombinationIds)) {
            return;
        }

        foreach ($shopCombinationIds as $shopCombinationId) {
            // Copy values from one shop to another, only copy data from Combination, the stock will be handled in copyCombinationsStockToShop
            $this->combinationRepository->copyToShop($shopCombinationId, $sourceShopId, $targetShopId);
        }

        if (!$this->combinationRepository->findDefaultCombinationIdForShop($productId, $targetShopId)) {
            $shopConstraint = ShopConstraint::shop($targetShopId->getValue());
            $firstCombinationId = $this->combinationRepository->findFirstCombinationId($productId, $shopConstraint);
            $this->defaultCombinationUpdater->setDefaultCombination($firstCombinationId, $shopConstraint);
        }
    }
}
