<?php

/*
 * This file is part of the VITICORE2 package.
 *
 * (c) CP Creation <web@cpcreation.fr>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

namespace CpCreation\VitiCore\Shop;

use CpCreation\VitiCore\Bridge\Symfony\Bundle\Context\GroupContext;
use CpCreation\VitiCore\Product\Model\Product;
use CpCreation\VitiCore\Product\Model\ProductCategory;
use CpCreation\VitiCore\Product\Model\ProductCollection;
use CpCreation\VitiCore\Product\Model\ProductCollectionItem;
use CpCreation\VitiCore\Product\Model\ProductPriceGroup;
use CpCreation\VitiCore\Product\Model\ProductVariant;
use CpCreation\VitiCore\Product\Repository\ProductCategoryRepository;
use CpCreation\VitiCore\Product\Repository\ProductCollectionRepository;
use CpCreation\VitiCore\Product\Repository\ProductPriceGroupRepository;
use CpCreation\VitiCore\Product\Repository\ProductRepository;
use CpCreation\VitiCore\Shop\Model\ShopConfig;
use CpCreation\VitiCore\Shop\Repository\ShopConfigRepository;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;

final class ShopCatalogResolver
{
    public const SOURCE_SHOP = 'shop';
    public const SOURCE_CATEGORY = 'category';
    public const SOURCE_COLLECTION = 'collection';

    public function __construct(
        private readonly ProductCategoryRepository $productCategoryRepository,
        private readonly ProductRepository $productRepository,
        private readonly ProductCollectionRepository $productCollectionRepository,
        private readonly ProductPriceGroupRepository $productPriceGroupRepository,
        private readonly ShopConfigRepository $shopConfigRepository,
    ) {}

    public function getShopConfig(): ShopConfig
    {
        $config = $this->shopConfigRepository->findOne();
        if (!$config instanceof ShopConfig) {
            throw new \RuntimeException('Shop config not found');
        }

        return $config;
    }

    /**
     * @return ProductCategory[]
     */
    public function getVisibleCategories(): array
    {
        return $this->productCategoryRepository->findOnlyShop(GroupContext::$groupId);
    }

    /**
     * @return array{
     *     source: string,
     *     config: ShopConfig,
     *     categories: ProductCategory[],
     *     category: ProductCategory|null,
     *     isCurrentCategoryVisible: bool,
     *     showCategoryMenu: bool,
     *     shopProducts: Product[],
     *     shopProductGroups: array<int, array{category: ProductCategory, products: Product[]}>,
     *     allShopProducts: Product[],
     *     mobileBarProducts: Product[],
     *     schemaProducts: Product[],
     *     activePriceGroupId: string|null,
     *     collection: ProductCollection|null
     * }
     */
    public function resolveCatalog(string $source = self::SOURCE_SHOP, ?string $categorySlug = null, ?string $collectionCode = null): array
    {
        $source = $this->normalizeSource($source);
        $config = $this->getShopConfig();
        $categories = $this->getVisibleCategories();
        $activePriceGroupId = $this->resolveActivePriceGroupId();
        $groupFilterId = \is_string(GroupContext::$groupId) && '' !== GroupContext::$groupId ? GroupContext::$groupId : null;
        $showCategoryMenu = self::SOURCE_COLLECTION !== $source
            && (self::SOURCE_CATEGORY === $source || $config->isShowCategoryMenuFront());
        $collection = null;
        $category = null;
        $shopProducts = [];
        $shopProductGroups = [];
        $allShopProducts = [];

        if (self::SOURCE_COLLECTION === $source) {
            $collection = $this->resolveCollection($collectionCode);
            $shopProducts = $this->resolveCollectionProducts($collection, $groupFilterId, shopOnly: true);
            $allShopProducts = $shopProducts;
        } else {
            $category = $this->resolveCategory($categorySlug, $categories);
            if (null === $category && null !== $categorySlug && '' !== mb_trim($categorySlug)) {
                throw new NotFoundHttpException();
            }

            if (null === $category && null === $categorySlug && [] !== $categories) {
                $category = $categories[0];
            }

            if ($showCategoryMenu) {
                $shopProducts = null !== $category
                    ? $this->productRepository->findOnlyShop(GroupContext::$groupId, $category)
                    : [];
                $allShopProducts = $shopProducts;
            } else {
                $shopProductGroups = $this->buildShopProductGroups($categories);
                $allShopProducts = $this->flattenShopProductGroups($shopProductGroups);
                $shopProducts = $allShopProducts;
            }
        }

        $isCurrentCategoryVisible = $this->isCategoryVisibleInShop($category, $categories);

        return [
            'source' => $source,
            'config' => $config,
            'categories' => $categories,
            'category' => $category,
            'isCurrentCategoryVisible' => $isCurrentCategoryVisible,
            'showCategoryMenu' => $showCategoryMenu,
            'shopProducts' => $shopProducts,
            'shopProductGroups' => $shopProductGroups,
            'allShopProducts' => $allShopProducts,
            'mobileBarProducts' => $showCategoryMenu ? $shopProducts : $allShopProducts,
            'schemaProducts' => $showCategoryMenu ? ($isCurrentCategoryVisible ? $shopProducts : []) : $allShopProducts,
            'activePriceGroupId' => $activePriceGroupId,
            'collection' => $collection,
        ];
    }

    public function resolveActivePriceGroupId(): ?string
    {
        if (\is_string(GroupContext::$groupId) && '' !== GroupContext::$groupId) {
            return GroupContext::$groupId;
        }

        return $this->productPriceGroupRepository->getGroupByDefault()?->getId()?->toString();
    }

    public function isProductVisibleInShop(Product $product): bool
    {
        if ($product->isDisabled() || $product->isDeleted() || !$product->isShop()) {
            return false;
        }

        $category = $product->getCategory();
        if (!$category instanceof ProductCategory || !$category->isPageShop()) {
            return false;
        }

        $groupId = GroupContext::$groupId;
        if (!\is_string($groupId) || '' === $groupId) {
            return true;
        }

        foreach ($product->getGroups() as $group) {
            if ($group instanceof ProductPriceGroup && $group->getId()->toString() === $groupId) {
                return true;
            }
        }

        return false;
    }

    /**
     * @param ProductCategory[] $visibleCategories
     */
    public function isCategoryVisibleInShop(?ProductCategory $category, array $visibleCategories): bool
    {
        if (!$category instanceof ProductCategory) {
            return false;
        }

        $categoryId = $category->getId()->toString();

        foreach ($visibleCategories as $visibleCategory) {
            if ($visibleCategory instanceof ProductCategory
                && $visibleCategory->getId()->toString() === $categoryId) {
                return true;
            }
        }

        return false;
    }

    /**
     * @return Product[]
     */
    public function resolveCollectionProducts(?ProductCollection $collection, ?string $groupId = null, bool $shopOnly = false): array
    {
        if (!$collection instanceof ProductCollection || !$collection->isCurrentlyPublished()) {
            return [];
        }

        if ($collection->isAutomatic()) {
            return $this->productRepository->findPublishedForProductCollection(
                $collection,
                $groupId,
                $shopOnly,
                $collection->getAutomaticLimit()
            );
        }

        $products = [];
        $seen = [];

        foreach ($collection->getItems() as $item) {
            if (!$item instanceof ProductCollectionItem) {
                continue;
            }

            $product = $item->getProduct();
            if (!$product instanceof Product || !$this->isPublishedProduct($product, $groupId, $shopOnly)) {
                continue;
            }

            $id = $product->getId()->toString();
            if (isset($seen[$id])) {
                continue;
            }

            $products[] = $product;
            $seen[$id] = true;
        }

        $limit = $collection->getAutomaticLimit();
        if (null !== $limit && $limit > 0) {
            return \array_slice($products, 0, $limit);
        }

        return $products;
    }

    /**
     * @param ProductCategory[] $categories
     *
     * @return array<int, array{category: ProductCategory, products: Product[]}>
     */
    public function buildShopProductGroups(array $categories): array
    {
        $groups = [];

        foreach ($categories as $category) {
            if (!$category instanceof ProductCategory) {
                continue;
            }

            $groups[] = [
                'category' => $category,
                'products' => $this->productRepository->findOnlyShop(GroupContext::$groupId, $category),
            ];
        }

        return $groups;
    }

    /**
     * @param array<int, array{category: ProductCategory, products: Product[]}> $shopProductGroups
     *
     * @return Product[]
     */
    public function flattenShopProductGroups(array $shopProductGroups): array
    {
        $products = [];

        foreach ($shopProductGroups as $shopProductGroup) {
            foreach ($shopProductGroup['products'] as $product) {
                $products[] = $product;
            }
        }

        return $products;
    }

    /**
     * @param ProductCategory[] $categories
     */
    private function resolveCategory(?string $categorySlug, array $categories): ?ProductCategory
    {
        if (null === $categorySlug || '' === mb_trim($categorySlug)) {
            return null;
        }

        foreach ($categories as $category) {
            if ($category instanceof ProductCategory && $category->getSlug() === $categorySlug) {
                return $category;
            }
        }

        return null;
    }

    private function resolveCollection(?string $collectionCode): ?ProductCollection
    {
        if (null === $collectionCode || '' === mb_trim($collectionCode)) {
            return null;
        }

        $collection = $this->productCollectionRepository->findOneByCode($collectionCode);

        return $collection instanceof ProductCollection && $collection->isCurrentlyPublished()
            ? $collection
            : null;
    }

    private function normalizeSource(string $source): string
    {
        return match ($source) {
            self::SOURCE_CATEGORY, self::SOURCE_COLLECTION => $source,
            default => self::SOURCE_SHOP,
        };
    }

    private function isPublishedProduct(Product $product, ?string $groupId = null, bool $shopOnly = false): bool
    {
        if ($product->isDisabled() || $product->isDeleted()) {
            return false;
        }

        if ($shopOnly && !$this->isProductVisibleInShop($product)) {
            return false;
        }

        if (null !== $groupId && '' !== $groupId) {
            $hasGroup = false;
            foreach ($product->getGroups() as $group) {
                if ($group instanceof ProductPriceGroup && $group->getId()->toString() === $groupId) {
                    $hasGroup = true;
                    break;
                }
            }

            if (!$hasGroup) {
                return false;
            }
        }

        foreach ($product->getVariants() as $variant) {
            if ($variant instanceof ProductVariant && !$variant->isUnPublished() && null === $variant->getDeletedAt()) {
                return true;
            }
        }

        return false;
    }
}
