<?php

namespace Autogedal\Labels\Model;

use Autogedal\Labels\Helper\Config;
use Magento\Catalog\Helper\Data as CatalogHelper;
use Magento\Catalog\Api\Data\ProductInterface;
use Magento\Catalog\Model\Product;
use Magento\CatalogInventory\Api\StockRegistryInterface;
use Magento\Framework\Pricing\PriceCurrencyInterface;

class LabelResolver
{
    private $config;
    private $labelEvaluator;
    private $stockRegistry;
    private $dateWindowMatcher;
    private $svgLabelVariantGenerator;
    private $priceCurrency;
    private $catalogHelper;
    /**
     * @var array<int, array{0: float, 1: float}>
     */
    private $comparablePricesCache = [];
    /**
     * @var array<int, mixed>
     */
    private $stockItemCache = [];
    /**
     * @var \DateTimeInterface|null
     */
    private $currentDate = null;

    public function __construct(
        Config $config,
        LabelEvaluator $labelEvaluator,
        StockRegistryInterface $stockRegistry,
        DateWindowMatcher $dateWindowMatcher,
        SvgLabelVariantGenerator $svgLabelVariantGenerator,
        PriceCurrencyInterface $priceCurrency,
        CatalogHelper $catalogHelper
    ) {
        $this->config = $config;
        $this->labelEvaluator = $labelEvaluator;
        $this->stockRegistry = $stockRegistry;
        $this->dateWindowMatcher = $dateWindowMatcher;
        $this->svgLabelVariantGenerator = $svgLabelVariantGenerator;
        $this->priceCurrency = $priceCurrency;
        $this->catalogHelper = $catalogHelper;
    }

    /**
     * @return LabelView[]
     */
    public function getLabels(ProductInterface $product, string $context = 'pdp'): array
    {
        if (!$this->config->isEnabled()) {
            return [];
        }

        $labels = [];

        foreach ($this->getCustomLabels($product) as $customLabel) {
            $labels[] = $customLabel;
        }

        return $labels;
    }

    /**
     * @param ProductInterface $product
     * @param string $context 'pdp' or 'plp'
     * @return LabelView[]
     */
    private function getCustomLabels(ProductInterface $product): array
    {
        $labels = [];
        $seenLabelIds = [];
        foreach ($this->labelEvaluator->getApplicableLabels($product, 'prod') as $label) {
            /** @var \Autogedal\Labels\Model\Label $label */
            $view = $this->buildCustomLabelView($label, $product);
            if ($view === null) {
                continue;
            }

            $seenLabelIds[(int)$label->getId()] = true;
            $labels[] = $view;
        }

        foreach ($this->getInheritedCustomLabels($product, $seenLabelIds) as $view) {
            $labels[] = $view;
        }

        return $labels;
    }

    /**
     * @param array<int, bool> $seenLabelIds
     * @return LabelView[]
     */
    private function getInheritedCustomLabels(ProductInterface $product, array $seenLabelIds): array
    {
        $result = [];
        $children = $this->getChildProducts($product);
        if ($children === []) {
            return $result;
        }

        foreach ($children as $childProduct) {
            if (!$childProduct instanceof ProductInterface) {
                continue;
            }

            foreach ($this->labelEvaluator->getApplicableLabels($childProduct, 'prod') as $label) {
                if ((int)$label->getData('use_for_parent') !== 1) {
                    continue;
                }

                $labelId = (int)$label->getId();
                if (isset($seenLabelIds[$labelId])) {
                    continue;
                }

                $view = $this->buildCustomLabelView($label, $childProduct);
                if ($view === null) {
                    continue;
                }

                $seenLabelIds[$labelId] = true;
                $result[] = $view;
            }
        }

        return $result;
    }

    private function buildCustomLabelView(Label $label, ProductInterface $product): ?LabelView
    {
        $text = (string)$label->getData('prod_txt');
        $cssClass = 'agd-label agd-label--custom agd-label--prod';
        $image = (string)$label->getData('prod_img');
        $color = (string)$label->getData('prod_label_color');
        $style = (string)$label->getData('prod_style');
        $textStyle = (string)$label->getData('prod_text_style');
        $imageWidth = (string)$label->getData('prod_image_width');
        $imageHeight = (string)$label->getData('prod_image_height');
        $position = (int)$label->getData('prod_pos');

        // Map position codes (prod_pos) to CSS classes
        $positionClass = $this->mapPositionClass($position);
        if ($positionClass !== null) {
            // label-table2 + positional class (e.g. top-left, middle-right)
            $cssClass .= ' label-table2 ' . $positionClass;
        }

        // Support basic dynamic placeholders such as {SAVE_PERCENT}
        // so prod_txt values keep their original behaviour.
        $text = $this->applyDynamicPlaceholders($text, $product);

        if ($image !== '' && $color !== '') {
            $image = $this->svgLabelVariantGenerator->getVariantPath($image, $color);
        }

        if ($text === '' && $image === '') {
            return null;
        }

        return new LabelView(
            'custom_' . (int)$label->getId(),
            $text,
            $cssClass,
            'custom',
            $image ?: null,
            $style ?: null,
            $textStyle ?: null,
            $imageWidth ?: null,
            $imageHeight ?: null
        );
    }

    /**
     * @return ProductInterface[]
     */
    private function getChildProducts(ProductInterface $product): array
    {
        if (!$product instanceof Product) {
            return [];
        }

        $typeId = (string)$product->getTypeId();
        if ($typeId === 'configurable') {
            try {
                $children = $product->getTypeInstance()->getUsedProducts($product);
                return is_array($children) ? $children : [];
            } catch (\Throwable $e) {
                return [];
            }
        }

        if ($typeId === 'grouped') {
            try {
                $children = $product->getTypeInstance()->getAssociatedProducts($product);
                return is_array($children) ? $children : [];
            } catch (\Throwable $e) {
                return [];
            }
        }

        return [];
    }

    /**
     * Replace simple dynamic variables in label text using product data.
     *
     * prod_txt values like "{SAVE_PERCENT}" still work.
     */
    private function applyDynamicPlaceholders(string $text, ProductInterface $product): string
    {
        if ($text === '') {
            return $text;
        }

        if (!preg_match_all('/\{([A-Z_][A-Z0-9:_-]*)\}/i', $text, $matches)) {
            return $text;
        }

        $replacements = [];
        foreach ($matches[1] as $token) {
            $replacements['{' . $token . '}'] = $this->resolvePlaceholder((string)$token, $product);
        }

        $text = strtr($text, $replacements);

        return $text;
    }

    private function resolvePlaceholder(string $token, ProductInterface $product): string
    {
        $upperToken = strtoupper($token);

        switch ($upperToken) {
            case 'BR':
                return '<br/>';
            case 'PRICE':
                return $this->formatPriceValue($this->getComparablePrices($product)[0]);
            case 'SPECIAL_PRICE':
                return $this->formatPriceValue((float)$product->getData('special_price'));
            case 'FINAL_PRICE':
                return $this->formatPriceValue($this->getComparablePrices($product)[1]);
            case 'FINAL_PRICE_INCL_TAX':
                return $this->formatPriceValue((float)$this->catalogHelper->getTaxPrice($product, $this->getComparablePrices($product)[1], true));
            case 'STARTINGFROM_PRICE':
                return $this->formatPriceValue((float)$product->getMinimalPrice());
            case 'STARTINGTO_PRICE':
                return $this->formatPriceValue((float)$product->getMaximalPrice());
            case 'SAVE_AMOUNT':
                [$regularPrice, $finalPrice] = $this->getComparablePrices($product);
                return $this->formatPriceValue(max(0.0, $regularPrice - $finalPrice));
            case 'SAVE_PERCENT':
            case 'DISCOUNT':
                return (string)((int)$this->getDiscountPercent($product)) . '%';
            case 'SKU':
                return (string)$product->getSku();
            case 'STOCK':
                return $this->formatStockQty($product);
            case 'NEW_FOR':
                return (string)$this->getDaysSinceCreated($product);
            case 'SPDL':
                return (string)$this->getSpecialPriceDaysLeft($product);
            case 'SPHL':
                return (string)$this->getSpecialPriceHoursLeft($product);
        }

        if (str_starts_with($upperToken, 'ATTR:')) {
            $attrCode = trim(substr($token, 5));
            if ($attrCode !== '') {
                $value = $product->getData($attrCode);
                if (is_array($value)) {
                    $value = implode(',', array_map('strval', $value));
                }
                return (string)$value;
            }
        }

        return '';
    }

    private function formatPriceValue(float $price): string
    {
        if ($price <= 0.0) {
            return '';
        }

        return strip_tags($this->priceCurrency->convertAndFormat($price, true));
    }

    private function formatStockQty(ProductInterface $product): string
    {
        $productId = (int)$product->getId();
        if (!$productId) {
            return '';
        }

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

    private function getDaysSinceCreated(ProductInterface $product): int
    {
        $createdAt = $product->getCreatedAt();
        if (!$createdAt) {
            return 0;
        }

        $created = $this->dateWindowMatcher->createDate($createdAt, false);
        $now = $this->getCurrentDate();
        if (!$created || !$now) {
            return 0;
        }

        return max(1, (int)floor(($now->getTimestamp() - $created->getTimestamp()) / 86400));
    }

    private function getSpecialPriceDaysLeft(ProductInterface $product): int
    {
        $toDate = $product->getData('special_to_date');
        if (!$toDate) {
            return 0;
        }

        $end = $this->dateWindowMatcher->createDate($toDate, true);
        $now = $this->getCurrentDate();
        if (!$end || !$now) {
            return 0;
        }

        return (int)floor(($end->getTimestamp() - $now->getTimestamp()) / 86400);
    }

    private function getSpecialPriceHoursLeft(ProductInterface $product): int
    {
        $toDate = $product->getData('special_to_date');
        if (!$toDate) {
            return 0;
        }

        $end = $this->dateWindowMatcher->createDate($toDate, true);
        $now = $this->getCurrentDate();
        if (!$end || !$now) {
            return 0;
        }

        return (int)floor(($end->getTimestamp() - $now->getTimestamp()) / 3600);
    }

    private function getDiscountPercent(ProductInterface $product): float
    {
        if (!$product instanceof Product) {
            return 0.0;
        }

        [$regularPrice, $finalPrice] = $this->getComparablePrices($product);
        if ($regularPrice <= 0.0 || $finalPrice <= 0.0 || $finalPrice >= $regularPrice) {
            return 0.0;
        }

        return round((($regularPrice - $finalPrice) / $regularPrice) * 100);
    }

    /**
     * Resolve comparable regular/final prices using the product price info first,
     * then fall back to direct product price fields when needed.
     *
     * @return float[]
     */
    private function getComparablePrices(Product $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 = (float)$product->getMinimalPrice();
            if ($minimalFinalPrice > 0.0) {
                $finalPrice = $minimalFinalPrice;
            }
        }

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

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

    private function getCurrentDate(): \DateTimeInterface
    {
        if ($this->currentDate === null) {
            $currentDate = $this->dateWindowMatcher->createDate(new \DateTime(), false);
            $this->currentDate = $currentDate ?: new \DateTime();
        }

        return $this->currentDate;
    }

    /**
     * Map position codes (prod_pos) to CSS classes.
     *
     * The exact mapping isn't documented, so we approximate based on the
     * imported data and original HTML samples.
     */
    private function mapPositionClass(int $position): ?string
    {
        switch ($position) {
            case 0:
                return 'top-left';
            case 1:
                return 'top-center';
            case 2:
                return 'top-right';
            case 3:
                return 'middle-left';
            case 4:
                return 'middle-center';
            case 5:
                return 'middle-right';
            case 6:
                return 'bottom-left';
            case 7:
                return 'bottom-center';
            case 8:
                return 'bottom-right';
            default:
                return null;
        }
    }
}
