<?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\QueryHandler;

use Customization;
use PrestaShop\PrestaShop\Adapter\Attachment\AttachmentRepository;
use PrestaShop\PrestaShop\Adapter\Category\Repository\CategoryRepository;
use PrestaShop\PrestaShop\Adapter\Configuration;
use PrestaShop\PrestaShop\Adapter\Product\Image\ProductImagePathFactory;
use PrestaShop\PrestaShop\Adapter\Product\Image\Repository\ProductImageRepository;
use PrestaShop\PrestaShop\Adapter\Product\Pack\Repository\ProductPackRepository;
use PrestaShop\PrestaShop\Adapter\Product\Repository\ProductRepository;
use PrestaShop\PrestaShop\Adapter\Product\SpecificPrice\Repository\SpecificPriceRepository;
use PrestaShop\PrestaShop\Adapter\Product\Stock\Repository\StockAvailableRepository;
use PrestaShop\PrestaShop\Adapter\Product\VirtualProduct\Repository\VirtualProductFileRepository;
use PrestaShop\PrestaShop\Adapter\SEO\RedirectTargetProvider;
use PrestaShop\PrestaShop\Adapter\Tax\TaxComputer;
use PrestaShop\PrestaShop\Core\Category\NameBuilder\CategoryDisplayNameBuilder;
use PrestaShop\PrestaShop\Core\CommandBus\Attributes\AsQueryHandler;
use PrestaShop\PrestaShop\Core\Domain\Attachment\QueryResult\AttachmentInformation;
use PrestaShop\PrestaShop\Core\Domain\Category\Exception\CategoryNotFoundException;
use PrestaShop\PrestaShop\Core\Domain\Category\ValueObject\CategoryId;
use PrestaShop\PrestaShop\Core\Domain\Country\ValueObject\CountryId;
use PrestaShop\PrestaShop\Core\Domain\Language\ValueObject\LanguageId;
use PrestaShop\PrestaShop\Core\Domain\Product\Exception\ProductNotFoundException;
use PrestaShop\PrestaShop\Core\Domain\Product\ProductCustomizabilitySettings;
use PrestaShop\PrestaShop\Core\Domain\Product\Query\GetProductForEditing;
use PrestaShop\PrestaShop\Core\Domain\Product\QueryHandler\GetProductForEditingHandlerInterface;
use PrestaShop\PrestaShop\Core\Domain\Product\QueryResult\CategoriesInformation;
use PrestaShop\PrestaShop\Core\Domain\Product\QueryResult\CategoryInformation;
use PrestaShop\PrestaShop\Core\Domain\Product\QueryResult\LocalizedTags;
use PrestaShop\PrestaShop\Core\Domain\Product\QueryResult\ProductBasicInformation;
use PrestaShop\PrestaShop\Core\Domain\Product\QueryResult\ProductCustomizationOptions;
use PrestaShop\PrestaShop\Core\Domain\Product\QueryResult\ProductDetails;
use PrestaShop\PrestaShop\Core\Domain\Product\QueryResult\ProductForEditing;
use PrestaShop\PrestaShop\Core\Domain\Product\QueryResult\ProductOptions;
use PrestaShop\PrestaShop\Core\Domain\Product\QueryResult\ProductPricesInformation;
use PrestaShop\PrestaShop\Core\Domain\Product\QueryResult\ProductSeoOptions;
use PrestaShop\PrestaShop\Core\Domain\Product\QueryResult\ProductShippingInformation;
use PrestaShop\PrestaShop\Core\Domain\Product\QueryResult\ProductStockInformation;
use PrestaShop\PrestaShop\Core\Domain\Product\Stock\Exception\StockAvailableNotFoundException;
use PrestaShop\PrestaShop\Core\Domain\Product\ValueObject\ProductId;
use PrestaShop\PrestaShop\Core\Domain\Product\VirtualProductFile\Exception\VirtualProductFileNotFoundException;
use PrestaShop\PrestaShop\Core\Domain\Product\VirtualProductFile\QueryResult\VirtualProductFileForEditing;
use PrestaShop\PrestaShop\Core\Domain\Shop\ValueObject\ShopConstraint;
use PrestaShop\PrestaShop\Core\Domain\Shop\ValueObject\ShopId;
use PrestaShop\PrestaShop\Core\Domain\TaxRulesGroup\ValueObject\TaxRulesGroupId;
use PrestaShop\PrestaShop\Core\Util\DateTime\DateTime as DateTimeUtil;
use PrestaShop\PrestaShop\Core\Util\Number\NumberExtractor;
use PrestaShop\PrestaShop\Core\Util\Number\NumberExtractorException;
use Product;
use Tag;

/**
 * Handles the query @see GetProductForEditing using legacy ObjectModel
 */
#[AsQueryHandler]
class GetProductForEditingHandler implements GetProductForEditingHandlerInterface
{
    /**
     * @var NumberExtractor
     */
    private $numberExtractor;

    /**
     * @var ProductRepository
     */
    private $productRepository;

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

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

    /**
     * @var VirtualProductFileRepository
     */
    private $virtualProductFileRepository;

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

    /**
     * @var TaxComputer
     */
    private $taxComputer;

    /**
     * @var int
     */
    private $countryId;

    /**
     * @var RedirectTargetProvider
     */
    private $targetProvider;

    /**
     * @var ProductImagePathFactory
     */
    private $productImageUrlFactory;

    /**
     * @var AttachmentRepository
     */
    private $attachmentRepository;

    /**
     * @var SpecificPriceRepository
     */
    private $specificPriceRepository;

    /**
     * @var Configuration
     */
    private $configuration;

    /**
     * @var CategoryDisplayNameBuilder
     */
    private $categoryDisplayNameBuilder;

    /**
     * @param NumberExtractor $numberExtractor
     * @param ProductRepository $productRepository
     * @param CategoryRepository $categoryRepository
     * @param StockAvailableRepository $stockAvailableRepository
     * @param VirtualProductFileRepository $virtualProductFileRepository
     * @param ProductImageRepository $productImageRepository
     * @param AttachmentRepository $attachmentRepository
     * @param TaxComputer $taxComputer
     * @param int $countryId
     * @param RedirectTargetProvider $targetProvider
     * @param ProductImagePathFactory $productImageUrlFactory
     * @param SpecificPriceRepository $specificPriceRepository
     * @param Configuration $configuration
     * @param CategoryDisplayNameBuilder $categoryDisplayNameBuilder
     */
    public function __construct(
        NumberExtractor $numberExtractor,
        ProductRepository $productRepository,
        CategoryRepository $categoryRepository,
        StockAvailableRepository $stockAvailableRepository,
        VirtualProductFileRepository $virtualProductFileRepository,
        ProductImageRepository $productImageRepository,
        AttachmentRepository $attachmentRepository,
        TaxComputer $taxComputer,
        int $countryId,
        RedirectTargetProvider $targetProvider,
        ProductImagePathFactory $productImageUrlFactory,
        SpecificPriceRepository $specificPriceRepository,
        Configuration $configuration,
        CategoryDisplayNameBuilder $categoryDisplayNameBuilder,
        private ProductPackRepository $packRepository,
    ) {
        $this->numberExtractor = $numberExtractor;
        $this->productRepository = $productRepository;
        $this->categoryRepository = $categoryRepository;
        $this->stockAvailableRepository = $stockAvailableRepository;
        $this->virtualProductFileRepository = $virtualProductFileRepository;
        $this->taxComputer = $taxComputer;
        $this->countryId = $countryId;
        $this->attachmentRepository = $attachmentRepository;
        $this->targetProvider = $targetProvider;
        $this->productImageRepository = $productImageRepository;
        $this->productImageUrlFactory = $productImageUrlFactory;
        $this->specificPriceRepository = $specificPriceRepository;
        $this->configuration = $configuration;
        $this->categoryDisplayNameBuilder = $categoryDisplayNameBuilder;
    }

    /**
     * {@inheritdoc}
     */
    public function handle(GetProductForEditing $query): ProductForEditing
    {
        $product = $this->productRepository->getByShopConstraint(
            $query->getProductId(),
            $query->getShopConstraint()
        );
        $productType = $product->getProductType();

        return new ProductForEditing(
            (int) $product->id,
            $productType,
            (bool) $product->active,
            $this->getCustomizationOptions($product),
            $this->getBasicInformation($product),
            $this->getCategoriesInformation($product, $query->getDisplayLanguageId()),
            $this->getPricesInformation($product, $query->getShopConstraint()),
            $this->getOptions($product),
            $this->getDetails($product),
            $this->getShippingInformation($product),
            $this->getSeoOptions($product),
            $this->getAttachments($query->getProductId()),
            $this->getProductStockInformation($product, $query->getShopConstraint()),
            $this->getVirtualProductFile($product),
            $this->getCover($query->getProductId(), $product->getShopId()),
            array_map(fn (ShopId $shopId) => $shopId->getValue(), $this->productRepository->getAssociatedShopIds($query->getProductId()))
        );
    }

    /**
     * @param ProductId $productId
     *
     * @return AttachmentInformation[]
     */
    private function getAttachments(ProductId $productId): array
    {
        $attachments = $this->attachmentRepository->getProductAttachments($productId);

        $attachmentsInfo = [];
        foreach ($attachments as $attachment) {
            $attachmentsInfo[] = new AttachmentInformation(
                (int) $attachment['id_attachment'],
                $attachment['name'],
                $attachment['description'],
                $attachment['file_name'],
                $attachment['mime'],
                (int) $attachment['file_size']
            );
        }

        return $attachmentsInfo;
    }

    /**
     * @param Product $product
     *
     * @return ProductBasicInformation
     */
    private function getBasicInformation(Product $product): ProductBasicInformation
    {
        return new ProductBasicInformation(
            $product->name,
            $product->description,
            $product->description_short,
            $this->getLocalizedTagsList((int) $product->id)
        );
    }

    /**
     * @param Product $product
     * @param LanguageId $languageId
     *
     * @return CategoriesInformation
     */
    private function getCategoriesInformation(Product $product, LanguageId $languageId): CategoriesInformation
    {
        $shopId = new ShopId($product->getShopId());
        $productId = new ProductId((int) $product->id);

        $categoryIds = $this->categoryRepository->getProductCategoryIds($productId, ShopConstraint::shop($shopId->getValue()));
        $defaultCategoryId = (int) $product->id_category_default;

        $categoryNames = $this->categoryRepository->getLocalizedNames($categoryIds);

        $categoriesInformation = [];
        foreach ($categoryNames as $categoryId => $localizedNames) {
            $categoryName = $categoryNames[$categoryId][$languageId->getValue()];
            $displayName = $this->categoryDisplayNameBuilder->build(
                $categoryName,
                $shopId,
                $languageId,
                new CategoryId($categoryId)
            );
            $categoriesInformation[] = new CategoryInformation(
                $categoryId,
                $categoryName,
                $displayName
            );
        }

        return new CategoriesInformation($categoriesInformation, $defaultCategoryId);
    }

    /**
     * @param Product $product
     * @param ShopConstraint $shopConstraint
     *
     * @return ProductPricesInformation
     *
     * @throws NumberExtractorException
     */
    private function getPricesInformation(Product $product, ShopConstraint $shopConstraint): ProductPricesInformation
    {
        $productId = new ProductId((int) $product->id);

        $taxEnabled = (bool) $this->configuration->get('PS_TAX', null, $shopConstraint);
        $ecotaxEnabled = (bool) $this->configuration->get('PS_USE_ECOTAX', null, $shopConstraint);
        $ecoTaxGroupId = (int) $this->configuration->get('PS_ECOTAX_TAX_RULES_GROUP_ID', null, $shopConstraint);
        $priceTaxExcluded = $this->numberExtractor->extract($product, 'price');
        $unitPriceTaxExcluded = $this->numberExtractor->extract($product, 'unit_price');
        $ecotaxTaxExcluded = $this->numberExtractor->extract($product, 'ecotax');

        if ($taxEnabled) {
            $priceTaxIncluded = $this->taxComputer->computePriceWithTaxes(
                $priceTaxExcluded,
                new TaxRulesGroupId((int) $product->id_tax_rules_group),
                new CountryId($this->countryId)
            );
            $unitPriceTaxIncluded = $this->taxComputer->computePriceWithTaxes(
                $unitPriceTaxExcluded,
                new TaxRulesGroupId((int) $product->id_tax_rules_group),
                new CountryId($this->countryId)
            );
            $ecotaxTaxIncluded = $this->taxComputer->computePriceWithTaxes(
                $ecotaxTaxExcluded,
                new TaxRulesGroupId($ecoTaxGroupId),
                new CountryId($this->countryId)
            );
        } else {
            $priceTaxIncluded = $priceTaxExcluded;
            $unitPriceTaxIncluded = $unitPriceTaxExcluded;
            $ecotaxTaxIncluded = $ecotaxTaxExcluded;
        }

        // Ecotax is applied independently of tax enabled
        if ($ecotaxEnabled) {
            $priceTaxIncluded = $priceTaxIncluded->plus($ecotaxTaxIncluded);
        }

        return new ProductPricesInformation(
            $priceTaxExcluded,
            $priceTaxIncluded,
            $ecotaxTaxExcluded,
            $ecotaxTaxIncluded,
            (int) $product->id_tax_rules_group,
            (bool) $product->on_sale,
            $this->numberExtractor->extract($product, 'wholesale_price'),
            $unitPriceTaxExcluded,
            $unitPriceTaxIncluded,
            (string) $product->unity,
            $this->numberExtractor->extract($product, 'unit_price_ratio'),
            $this->specificPriceRepository->findPrioritiesForProduct($productId)
        );
    }

    /**
     * @param Product $product
     *
     * @return ProductOptions
     */
    private function getOptions(Product $product): ProductOptions
    {
        return new ProductOptions(
            $product->visibility,
            (bool) $product->available_for_order,
            (bool) $product->online_only,
            (bool) $product->show_price,
            $product->condition,
            (bool) $product->show_condition,
            (int) $product->id_manufacturer
        );
    }

    /**
     * @param Product $product
     *
     * @return ProductDetails
     */
    private function getDetails(Product $product): ProductDetails
    {
        return new ProductDetails(
            $product->isbn,
            $product->upc,
            $product->ean13,
            $product->mpn,
            $product->reference
        );
    }

    /**
     * @param Product $product
     *
     * @return ProductShippingInformation
     *
     * @throws NumberExtractorException
     */
    private function getShippingInformation(Product $product): ProductShippingInformation
    {
        $carrierReferences = array_map(function ($carrier): int {
            return (int) $carrier['id_reference'];
        }, $product->getCarriers());

        return new ProductShippingInformation(
            $this->numberExtractor->extract($product, 'width'),
            $this->numberExtractor->extract($product, 'height'),
            $this->numberExtractor->extract($product, 'depth'),
            $this->numberExtractor->extract($product, 'weight'),
            $this->numberExtractor->extract($product, 'additional_shipping_cost'),
            $carrierReferences,
            (int) $product->additional_delivery_times,
            $product->delivery_in_stock,
            $product->delivery_out_stock
        );
    }

    /**
     * @param int $productId
     *
     * @return LocalizedTags[]
     */
    private function getLocalizedTagsList(int $productId): array
    {
        $tags = Tag::getProductTags($productId);

        if (!$tags) {
            return [];
        }

        $localizedTagsList = [];

        foreach ($tags as $langId => $localizedTags) {
            $localizedTagsList[] = new LocalizedTags((int) $langId, $localizedTags);
        }

        return $localizedTagsList;
    }

    /**
     * @param Product $product
     *
     * @return ProductCustomizationOptions
     */
    private function getCustomizationOptions(Product $product): ProductCustomizationOptions
    {
        if (!Customization::isFeatureActive()) {
            return ProductCustomizationOptions::createNotCustomizable();
        }

        $textFieldsCount = (int) $product->text_fields;
        $fileFieldsCount = (int) $product->uploadable_files;

        switch ((int) $product->customizable) {
            case ProductCustomizabilitySettings::ALLOWS_CUSTOMIZATION:
                $options = ProductCustomizationOptions::createAllowsCustomization($textFieldsCount, $fileFieldsCount);
                break;
            case ProductCustomizabilitySettings::REQUIRES_CUSTOMIZATION:
                $options = ProductCustomizationOptions::createRequiresCustomization($textFieldsCount, $fileFieldsCount);
                break;
            default:
                $options = ProductCustomizationOptions::createNotCustomizable();
        }

        return $options;
    }

    /**
     * @param Product $product
     *
     * @return ProductSeoOptions
     */
    private function getSeoOptions(Product $product): ProductSeoOptions
    {
        $redirectTarget = null;
        try {
            $redirectTarget = $this->targetProvider->getRedirectTarget(
                $product->redirect_type,
                (int) $product->id_type_redirected
            );
        } catch (ProductNotFoundException|CategoryNotFoundException $e) {
            // If the redirect target (product or category) has been deleted,
            // we still allow loading the product but with no redirect target
            $redirectTarget = null;
        }

        return new ProductSeoOptions(
            $product->meta_title,
            $product->meta_description,
            $product->link_rewrite,
            $product->redirect_type,
            $redirectTarget
        );
    }

    /**
     * Returns the product stock infos, it's important that the Product is fetched with stock data
     *
     * @param Product $product
     * @param ShopConstraint $shopConstraint
     *
     * @return ProductStockInformation
     */
    private function getProductStockInformation(Product $product, ShopConstraint $shopConstraint): ProductStockInformation
    {
        try {
            $stockAvailable = $this->stockAvailableRepository->getForProduct(new ProductId($product->id), new ShopId($product->getShopId()));
        } catch (StockAvailableNotFoundException) {
            $stockAvailable = $this->stockAvailableRepository->createStockAvailable(new ProductId($product->id), new ShopId($product->getShopId()));
        }

        // Compute pack quantity based on its configuration
        $shopId = $shopConstraint->getShopId() !== null ? $shopConstraint->getShopId() : new ShopId($product->getShopId());
        $packQuantity = $this->packRepository->getDynamicPackQuantity(new ProductId((int) $product->id), $shopId);

        return new ProductStockInformation(
            (int) $product->pack_stock_type,
            (int) $stockAvailable->out_of_stock,
            (int) $stockAvailable->quantity,
            (int) $product->minimal_quantity,
            (int) $product->low_stock_threshold,
            (bool) $product->low_stock_alert,
            $product->available_now,
            $product->available_later,
            $stockAvailable->location,
            DateTimeUtil::buildDateTimeOrNull($product->available_date),
            $packQuantity,
        );
    }

    /**
     * Get virtual product file
     * Legacy object ProductDownload is referred as VirtualProductFile in Core
     *
     * @param Product $product
     *
     * @return VirtualProductFileForEditing|null
     */
    private function getVirtualProductFile(Product $product): ?VirtualProductFileForEditing
    {
        try {
            $virtualProductFile = $this->virtualProductFileRepository->findByProductId(new ProductId($product->id));
        } catch (VirtualProductFileNotFoundException) {
            return null;
        }

        return new VirtualProductFileForEditing(
            (int) $virtualProductFile->id,
            $virtualProductFile->filename,
            $virtualProductFile->display_filename,
            (int) $virtualProductFile->nb_days_accessible,
            (int) $virtualProductFile->nb_downloadable,
            DateTimeUtil::buildDateTimeOrNull($virtualProductFile->date_expiration)
        );
    }

    private function getCover(ProductId $productId, int $shopId): string
    {
        $idOfCoverImage = $this->productImageRepository->findCoverImageId($productId, new ShopId($shopId));

        if ($idOfCoverImage) {
            return $this->productImageUrlFactory->getPathByType($idOfCoverImage, ProductImagePathFactory::IMAGE_TYPE_CART_DEFAULT);
        }

        return $this->productImageUrlFactory->getNoImagePath(ProductImagePathFactory::IMAGE_TYPE_CART_DEFAULT);
    }
}
