<?php

namespace Autogedal\Labels\Model;

use Autogedal\Labels\Model\ResourceModel\Label\CollectionFactory;
use Magento\Catalog\Api\CategoryRepositoryInterface;
use Magento\Catalog\Helper\Data as CatalogHelper;
use Magento\Catalog\Api\Data\ProductInterface;
use Magento\Catalog\Model\Product;
use Magento\Catalog\Api\ProductRepositoryInterface;
use Magento\CatalogInventory\Api\StockRegistryInterface;
use Magento\Framework\Exception\NoSuchEntityException;
use Magento\Customer\Model\Session as CustomerSession;
use Magento\Store\Model\StoreManagerInterface;

class LabelEvaluator
{
    /** attr_rule: 0 = one of selected values, 1 = all selected values (M1-style) */
    private const ATTR_RULE_EQUALS = 0;
    private const ATTR_RULE_ALL_SELECTED = 1;

    private $collectionFactory;
    private $storeManager;
    private $customerSession;
    private $stockRegistry;
    private $dateWindowMatcher;
    private $catalogHelper;
    private $productRepository;
    private $categoryRepository;
    /**
     * @var Label[]|null
     */
    private $activeLabels = null;
    /**
     * @var array<string, Label[]>
     */
    private $applicableLabelsCache = [];
    /**
     * @var array<string, array{0: float, 1: float}>
     */
    private $comparablePricesCache = [];
    /**
     * @var array<int, mixed>
     */
    private $stockItemCache = [];
    /**
     * @var array<string, ProductInterface|null>
     */
    private $loadedProductCache = [];
    /**
     * @var array<string, mixed>
     */
    private $categoryCache = [];

    public function __construct(
        CollectionFactory $collectionFactory,
        StoreManagerInterface $storeManager,
        CustomerSession $customerSession,
        StockRegistryInterface $stockRegistry,
        DateWindowMatcher $dateWindowMatcher,
        CatalogHelper $catalogHelper,
        ProductRepositoryInterface $productRepository,
        CategoryRepositoryInterface $categoryRepository
    ) {
        $this->collectionFactory = $collectionFactory;
        $this->storeManager = $storeManager;
        $this->customerSession = $customerSession;
        $this->stockRegistry = $stockRegistry;
        $this->dateWindowMatcher = $dateWindowMatcher;
        $this->catalogHelper = $catalogHelper;
        $this->productRepository = $productRepository;
        $this->categoryRepository = $categoryRepository;
    }

    /**
     * @return Label[]
     */
    public function getApplicableLabels(ProductInterface $product, string $mode): array
    {
        $storeId = (int)$this->storeManager->getStore()->getId();
        $now = $this->dateWindowMatcher->createDate(new \DateTime(), false);
        $customerGroupId = (int)$this->customerSession->getCustomerGroupId();
        $productId = (int)$product->getId();
        $cacheKey = $productId > 0
            ? $productId . '|' . $storeId . '|' . $customerGroupId . '|' . $mode
            : spl_object_id($product) . '|' . $storeId . '|' . $customerGroupId . '|' . $mode;

        if (isset($this->applicableLabelsCache[$cacheKey])) {
            return $this->applicableLabelsCache[$cacheKey];
        }

        $result = [];

        /** @var Label $label */
        foreach ($this->getActiveLabels() as $label) {
            if (!$this->isApplicableToProduct($label, $product, $mode, $storeId, $now, $customerGroupId)) {
                continue;
            }
            $result[] = $label;
        }

        usort($result, function (Label $a, Label $b): int {
            $pa = (int)$a->getData('pos');
            $pb = (int)$b->getData('pos');
            if ($pa === $pb) {
                return (int)$a->getId() <=> (int)$b->getId();
            }
            return $pa <=> $pb;
        });

        // If any label is marked as single, keep the first one only.
        $single = array_filter($result, static function (Label $label): bool {
            return (int)$label->getData('is_single') === 1;
        });
        if (!empty($single)) {
            $result = [reset($single)];
        }

        $this->applicableLabelsCache[$cacheKey] = $result;

        return $result;
    }

    /**
     * @return Label[]
     */
    private function getActiveLabels(): array
    {
        if ($this->activeLabels !== null) {
            return $this->activeLabels;
        }

        /** @var \Autogedal\Labels\Model\ResourceModel\Label\Collection $collection */
        $collection = $this->collectionFactory->create();
        $collection->addFieldToFilter('is_active', 1);

        $labels = [];
        /** @var Label $label */
        foreach ($collection as $label) {
            $labels[] = $label;
        }

        $this->activeLabels = $labels;

        return $labels;
    }

    private function isApplicableToProduct(
        Label $label,
        ProductInterface $product,
        string $mode,
        int $storeId,
        \DateTimeInterface $now,
        int $customerGroupId
    ): bool {
        // Store scope check (stores is a CSV like ,1,2,3,)
        $stores = (string)$label->getData('stores');
        if ($stores !== '') {
            // treat empty or ",0," as all stores
            if (strpos($stores, ',0,') === false) {
                if (strpos($stores, ',' . $storeId . ',') === false) {
                    return false;
                }
            }
        }

        // Customer groups
        if ((int)$label->getData('customer_group_enabled') === 1) {
            $groupsCsv = (string)$label->getData('customer_groups');
            if ($groupsCsv !== '') {
                $groups = array_filter(array_map('trim', explode(',', $groupsCsv)));
                if ($groups && !in_array((string)$customerGroupId, $groups, true)) {
                    return false;
                }
            }
        }

        // Date range
        if ((int)$label->getData('date_range_enabled') === 1) {
            if (!$this->dateWindowMatcher->isCurrentWithin(
                $label->getData('from_date'),
                $label->getData('to_date'),
                $now
            )) {
                return false;
            }
        }

        // Price range
        if ((int)$label->getData('price_range_enabled') === 1) {
            $fromPrice = (float)$label->getData('from_price');
            $toPrice = (float)$label->getData('to_price');
            $priceMode = (int)$label->getData('by_price');
            $finalPrice = $this->getPriceByMode($product, $priceMode);

            if ($fromPrice && $finalPrice < $fromPrice) {
                return false;
            }
            if ($toPrice && $toPrice > 0 && $finalPrice > $toPrice) {
                return false;
            }
        }

        // Simple Is New / Is Sale flags:
        // 0 = doesn't matter, 1 = No, 2 = Yes
        if (!$this->matchSimpleNewSaleFlags($label, $product, $now)) {
            return false;
        }

        // Special price only: when 1, label applies only if product has special price
        if ((int)$label->getData('special_price_only') === 1) {
            [$regularPrice, $finalPrice] = $this->getComparablePrices($product);
            if ($regularPrice <= 0 || $finalPrice >= $regularPrice) {
                return false;
            }
        }

        // SKU include/exclude
        $includeType = (int)$label->getData('include_type');
        $includeSku = trim((string)$label->getData('include_sku'));
        if ($includeType === 2 && $includeSku === '') {
            // \"SKUs listed below only\" but list is empty => label should never apply
            return false;
        }
        if ($includeType && $includeSku !== '') {
            $skuList = array_filter(array_map('trim', preg_split('/\s*,\s*/', $includeSku)));
            $sku = (string)$product->getSku();
            if ($includeType === 1) {
                // all except SKUs
                if (in_array($sku, $skuList, true)) {
                    return false;
                }
            } elseif ($includeType === 2) {
                // SKUs only
                if (!in_array($sku, $skuList, true)) {
                    return false;
                }
            }
        }

        // Category filter (M1: when label has categories set, product must be in at least one)
        if ((int)$label->getData('category_enabled') === 1) {
            $categoryCsv = (string)$label->getData('category');
            if ($categoryCsv !== '') {
                $categoryIds = array_filter(array_map('intval', explode(',', $categoryCsv)));
                if (!empty($categoryIds)) {
                    $productCategoryIds = $product instanceof Product ? $product->getCategoryIds() : [];
                    if (empty($productCategoryIds) || !$this->matchesCategoryCondition($categoryIds, $productCategoryIds, $storeId)) {
                        return false;
                    }
                }
            }
        }

        // Attribute condition (M1: attr_code + attr_value + attr_rule, optionally attr_multi)
        $attrCode = trim((string)$label->getData('attr_code'));
        if ($attrCode !== '') {
            if (!$this->matchAttributeCondition($label, $product, $attrCode)) {
                return false;
            }
        }

        // Stock conditions (M1: product_stock_enabled, stock_status, stock_less, stock_more)
        if ((int)$label->getData('product_stock_enabled') === 1) {
            if (!$this->matchStockCondition($label, $product)) {
                return false;
            }
        }

        return true;
    }

    /**
     * Match direct product categories and anchor parent categories.
     *
     * If the label is attached to an anchor category, products assigned to child
     * categories of that anchor should also match.
     *
     * @param int[] $labelCategoryIds
     * @param int[] $productCategoryIds
     */
    private function matchesCategoryCondition(array $labelCategoryIds, array $productCategoryIds, int $storeId): bool
    {
        $productCategoryIds = array_values(array_unique(array_map('intval', $productCategoryIds)));
        if ($productCategoryIds === []) {
            return false;
        }

        foreach ($labelCategoryIds as $labelCategoryId) {
            $labelCategoryId = (int)$labelCategoryId;
            if ($labelCategoryId <= 0) {
                continue;
            }

            if (in_array($labelCategoryId, $productCategoryIds, true)) {
                return true;
            }

            $labelCategory = $this->getCategoryById($labelCategoryId, $storeId);
            if (!$labelCategory || (int)$labelCategory->getIsAnchor() !== 1) {
                continue;
            }

            $labelCategoryPath = (string)$labelCategory->getPath();
            if ($labelCategoryPath === '') {
                continue;
            }

            foreach ($productCategoryIds as $productCategoryId) {
                $productCategory = $this->getCategoryById((int)$productCategoryId, $storeId);
                if (!$productCategory) {
                    continue;
                }

                $productCategoryPath = (string)$productCategory->getPath();
                if ($productCategoryPath === '') {
                    continue;
                }

                if ($productCategoryPath === $labelCategoryPath || strpos($productCategoryPath, $labelCategoryPath . '/') === 0) {
                    return true;
                }
            }
        }

        return false;
    }

    private function getCategoryById(int $categoryId, int $storeId)
    {
        if ($categoryId <= 0) {
            return null;
        }

        $cacheKey = $storeId . '|' . $categoryId;
        if (array_key_exists($cacheKey, $this->categoryCache)) {
            return $this->categoryCache[$cacheKey];
        }

        try {
            $this->categoryCache[$cacheKey] = $this->categoryRepository->get($categoryId, $storeId);
        } catch (NoSuchEntityException $e) {
            $this->categoryCache[$cacheKey] = null;
        } catch (\Throwable $e) {
            $this->categoryCache[$cacheKey] = null;
        }

        return $this->categoryCache[$cacheKey];
    }

    /**
     * M1-style attribute rule: 0 = equals / in list, 1 = not equals, 2 = contains
     */
    private function matchAttributeCondition(Label $label, ProductInterface $product, string $attrCode): bool
    {
        $attrValue = (string)$label->getData('attr_value');
        $attrRule = (int)$label->getData('attr_rule');
        $attrMulti = (int)$label->getData('attr_multi') === 1;

        $productValue = $product->getData($attrCode);
        if ($productValue === null || $productValue === '') {
            $productValue = $this->loadAttributeValueFromRepository($product, $attrCode, $productValue);
        }

        if ($productValue === null) {
            $productValue = '';
        }
        // Normalize to comparable form: scalar to string, array to list of strings
        if (is_array($productValue)) {
            $productValues = array_map('strval', $productValue);
        } else {
            $productValues = $attrMulti ? array_map('trim', explode(',', (string)$productValue)) : [(string)$productValue];
        }
        $productValues = array_filter($productValues);

        $allowedValues = array_filter(array_map('trim', explode(',', $attrValue)));

        if ($attrMulti) {
            if ($attrRule === self::ATTR_RULE_ALL_SELECTED) {
                if (empty($allowedValues)) {
                    return true;
                }

                $missing = array_diff($allowedValues, $productValues);
                return empty($missing);
            }

            if (empty($allowedValues)) {
                return true;
            }

            foreach ($productValues as $pv) {
                if (in_array($pv, $allowedValues, true)) {
                    return true;
                }
            }

            return false;
        }

        if (empty($allowedValues)) {
            return true;
        }

        $singleValue = (string)reset($productValues);
        foreach ($allowedValues as $allowedValue) {
            if ($singleValue === $allowedValue) {
                return true;
            }
        }

        return false;
    }

    private function loadAttributeValueFromRepository(ProductInterface $product, string $attrCode, $currentValue)
    {
        if ($currentValue !== null && $currentValue !== '') {
            return $currentValue;
        }

        $productId = (int)$product->getId();
        if ($productId <= 0) {
            return $currentValue;
        }

        try {
            $storeId = (int)($product instanceof Product ? $product->getStoreId() : 0);
            if ($storeId <= 0) {
                $storeId = (int)$this->storeManager->getStore()->getId();
            }

            $cacheKey = $productId . '|' . $storeId;
            if (!array_key_exists($cacheKey, $this->loadedProductCache)) {
                $this->loadedProductCache[$cacheKey] = $this->productRepository->getById($productId, false, $storeId, true);
            }

            $fullProduct = $this->loadedProductCache[$cacheKey];
            if (!$fullProduct) {
                return $currentValue;
            }
            $loadedValue = $fullProduct->getData($attrCode);
            if ($loadedValue !== null && $loadedValue !== '') {
                return $loadedValue;
            }
        } catch (\Throwable $e) {
            return $currentValue;
        }

        return $currentValue;
    }

    /**
     * M1-style stock: stock_status 0=any, 1=out of stock only, 2=in stock only;
     * stock_less = don't show if qty < stock_less; stock_more = don't show if qty > stock_more
     */
    private function matchStockCondition(Label $label, ProductInterface $product): bool
    {
        $productId = (int)$product->getId();
        if (!$productId) {
            return true;
        }

        try {
            if (!array_key_exists($productId, $this->stockItemCache)) {
                $this->stockItemCache[$productId] = $this->stockRegistry->getStockItem($productId);
            }
            $stockItem = $this->stockItemCache[$productId];
        } catch (\Throwable $e) {
            return true;
        }

        $stockStatus = (int)$label->getData('stock_status');
        if ($stockStatus === 1 && $stockItem->getIsInStock()) {
            return false;
        }
        if ($stockStatus === 2 && !$stockItem->getIsInStock()) {
            return false;
        }

        $stockLess = (int)$label->getData('stock_less');
        $stockMore = (int)$label->getData('stock_more');
        $qty = (float)$stockItem->getQty();

        if ($stockLess >= 0 && $qty >= $stockLess) {
            return false;
        }
        if ($stockMore > 0 && $qty > $stockMore) {
            return false;
        }

        return true;
    }

    private function matchSimpleNewSaleFlags(
        Label $label,
        ProductInterface $product,
        \DateTimeInterface $now
    ): bool
    {
        $isNewFlag = (int)$label->getData('is_new');   // 0,1,2
        $isSaleFlag = (int)$label->getData('is_sale'); // 0,1,2

        [$regularPrice, $finalPrice] = $this->getComparablePrices($product);
        $hasDiscount = $regularPrice > 0 && $finalPrice < $regularPrice;

        // New: we approximate by using news_from/to attributes if set, else creation date within 30 days
        $isNew = false;
        $newsFrom = $product->getData('news_from_date');
        $newsTo = $product->getData('news_to_date');
        if ($this->dateWindowMatcher->isCurrentWithin($newsFrom, $newsTo, $now)) {
            $isNew = true;
        } else {
            $createdAt = $product->getCreatedAt();
            if ($createdAt) {
                $created = $this->dateWindowMatcher->createDate($createdAt, false);
                $diff = $now->diff($created);
                $isNew = $diff->days <= 30;
            }
        }

        if ($isNewFlag === 2 && !$isNew) {
            return false;
        }
        if ($isNewFlag === 1 && $isNew) {
            return false;
        }

        if ($isSaleFlag === 2 && !$hasDiscount) {
            return false;
        }
        if ($isSaleFlag === 1 && $hasDiscount) {
            return false;
        }

        return true;
    }

    /**
     * Resolve comparable regular/final prices using product pricing data first,
     * with fallbacks for products whose type-specific price model does not expose
     * a useful regular price through price info.
     *
     * @return float[]
     */
    private function getComparablePrices(ProductInterface $product): array
    {
        $cacheKey = spl_object_id($product);
        if (isset($this->comparablePricesCache[$cacheKey])) {
            return $this->comparablePricesCache[$cacheKey];
        }

        $regularPrice = 0.0;
        $finalPrice = 0.0;
        $isBundle = $product->getTypeId() === 'bundle';

        $priceInfo = $product->getPriceInfo();
        if ($priceInfo !== null) {
            $regularPricePrice = $priceInfo->getPrice('regular_price');
            $finalPricePrice = $priceInfo->getPrice('final_price');

            if ($isBundle) {
                $regularPrice = (float)$regularPricePrice->getMaximalPrice()->getValue();
                $finalPrice = (float)$finalPricePrice->getMaximalPrice()->getValue();
            } else {
                $regularPrice = (float)$regularPricePrice->getValue();
                $finalPrice = (float)$finalPricePrice->getValue();
            }
        }

        if ($regularPrice <= 0.0) {
            $regularPrice = (float)$product->getData('price');
        }

        if ($finalPrice <= 0.0) {
            $finalPrice = (float)$product->getData('final_price');
        }

        if ($regularPrice <= 0.0) {
            $minimalPrice = (float)$product->getData('min_price');
            if ($minimalPrice > 0.0) {
                $regularPrice = $minimalPrice;
            }
        }

        if ($finalPrice <= 0.0) {
            $minimalFinalPrice = $this->getMinimalPriceValue($product);
            if ($minimalFinalPrice > 0.0) {
                $finalPrice = $minimalFinalPrice;
            }
        }

        $this->comparablePricesCache[$cacheKey] = [$regularPrice, $finalPrice];

        return $this->comparablePricesCache[$cacheKey];
    }

    private function getGroupedMinimalPrice(?ProductInterface $product): float
    {
        if (!$product) {
            return 0.0;
        }

        if (!$product instanceof Product || $product->getTypeId() !== 'grouped') {
            return $this->getMinimalPriceValue($product);
        }

        try {
            $associatedProducts = $product->getTypeInstance()->getAssociatedProducts($product);
        } catch (\Throwable $e) {
            return $this->getMinimalPriceValue($product);
        }

        $minimum = null;
        foreach ($associatedProducts as $associatedProduct) {
            $childPrice = $this->getTaxPrice($associatedProduct, (float)$associatedProduct->getFinalPrice());
            if ($minimum === null || $childPrice < $minimum) {
                $minimum = $childPrice;
            }
        }

        return $minimum !== null ? $minimum : $this->getMinimalPriceValue($product);
    }

    private function getGroupedMaximalPrice(?ProductInterface $product): float
    {
        if (!$product) {
            return 0.0;
        }

        if (!$product instanceof Product || $product->getTypeId() !== 'grouped') {
            return $this->getMaximalPriceValue($product);
        }

        try {
            $associatedProducts = $product->getTypeInstance()->getAssociatedProducts($product);
        } catch (\Throwable $e) {
            return $this->getMaximalPriceValue($product);
        }

        $maximum = 0.0;
        foreach ($associatedProducts as $associatedProduct) {
            $childPrice = $this->getTaxPrice($associatedProduct, (float)$associatedProduct->getFinalPrice());
            $qty = (float)$associatedProduct->getQty();
            $maximum += $qty > 0 ? $childPrice * $qty : $childPrice;
        }

        return $maximum > 0.0 ? $maximum : $this->getMaximalPriceValue($product);
    }

    private function getTaxPrice(ProductInterface $product, float $amount): float
    {
        return (float)$this->catalogHelper->getTaxPrice($product, $amount, true);
    }

    private function getMinimalPriceValue(ProductInterface $product): float
    {
        if ($product instanceof Product) {
            return (float)$product->getMinimalPrice();
        }

        return (float)$product->getData('min_price');
    }

    private function getMaximalPriceValue(ProductInterface $product): float
    {
        if ($product instanceof Product) {
            return (float)$product->getMaximalPrice();
        }

        return (float)$product->getData('max_price');
    }

    private function getPriceByMode(ProductInterface $product, int $priceMode): float
    {
        [$regularPrice, $finalPrice] = $this->getComparablePrices($product);

        switch ($priceMode) {
            case 1:
                $price = (float)$product->getData('special_price');
                break;
            case 2:
                $price = $finalPrice;
                break;
            case 3:
                $price = $this->getTaxPrice($product, $finalPrice);
                break;
            case 4:
                $price = $this->getGroupedMinimalPrice($product);
                break;
            case 5:
                $price = $this->getGroupedMaximalPrice($product);
                break;
            case 0:
            default:
                $price = $regularPrice;
                break;
        }

        if ($price <= 0.0) {
            $price = $finalPrice > 0.0 ? $finalPrice : $regularPrice;
        }

        return $price;
    }
}
