<?php

namespace EasySales\Integrari\Model;

use EasySales\Integrari\Api\ProductManagementInterface;
use EasySales\Integrari\Core\Auth\CheckWebsiteToken;
use EasySales\Integrari\Core\EasySales;
use EasySales\Integrari\Core\Transformers\Product;
use EasySales\Integrari\Helper\Data;
use Magento\Catalog\Api\ProductRepositoryInterface;
use Magento\Catalog\Model\ProductFactory;
use Magento\Catalog\Model\ResourceModel\Product\CollectionFactory as ProductCollectionFactory;
use Magento\Framework\Api\SearchCriteriaBuilder;
use Magento\Framework\App\ResourceConnection;
use Magento\Framework\Webapi\Rest\Request as RequestInterface;
use Magento\Tax\Model\TaxCalculation;

class ProductManagement extends CheckWebsiteToken implements ProductManagementInterface
{
    /**
     * @var ProductRepositoryInterface
     */
    private $productRepository;

    /**
     * @var ProductFactory
     */
    private $productFactory;

    /**
     * @var SearchCriteriaBuilder
     */
    private $searchCriteria;

    /**
     * @var Product
     */
    private $productService;

    /**
     * @var mixed
     */
    private $sourceItemsBySku = null;

    /**
     * @var Data
     */
    protected $helperData;

    /**
     * @var mixed
     */
    private $defaultStockSource;

    /**
     * @var ResourceConnection
     */
    private $resourceConnection;

    /**
     * @var ProductCollectionFactory
     */
    private $productCollectionFactory;

    /**
     * @var TaxCalculation
     */
    private $taxCalculation;

    /**
     * @var bool
     */
    private $taxIncludedInPrice;

    /**
     * @param RequestInterface $request
     * @param ProductRepositoryInterface $productRepository
     * @param ProductFactory $productFactory
     * @param SearchCriteriaBuilder $searchCriteriaBuilder
     * @param Product $productService
     * @param Data $helperData
     * @param ResourceConnection $resourceConnection
     * @param ProductCollectionFactory $productCollectionFactory
     * @param TaxCalculation $taxCalculation
     * @param \Magento\Framework\Module\Manager $moduleManager
     * @param \Magento\Framework\ObjectManagerInterface $objectManager
     * @throws \Exception
     */
    public function __construct(
        RequestInterface $request,
        ProductRepositoryInterface $productRepository,
        ProductFactory $productFactory,
        SearchCriteriaBuilder $searchCriteriaBuilder,
        Product $productService,
        Data $helperData,
        ResourceConnection $resourceConnection,
        ProductCollectionFactory $productCollectionFactory,
        TaxCalculation $taxCalculation,
        \Magento\Framework\Module\Manager $moduleManager,
        \Magento\Framework\ObjectManagerInterface $objectManager
    ) {
        parent::__construct($request, $helperData);

        $this->productRepository = $productRepository;
        $this->productFactory = $productFactory;
        $this->searchCriteria = $searchCriteriaBuilder;
        $this->productService = $productService;
        $this->resourceConnection = $resourceConnection;
        $this->productCollectionFactory = $productCollectionFactory;
        $this->taxCalculation = $taxCalculation;

        if ($moduleManager->isEnabled('Magento_Inventory') && $moduleManager->isEnabled('Magento_InventoryApi')) {
            $this->sourceItemsBySku = $objectManager->create('Magento\InventoryApi\Api\GetSourceItemsBySkuInterface');
        }

        $this->helperData = $helperData;
        $this->defaultStockSource = $this->helperData->getGeneralConfig('stock_source');

        $this->taxIncludedInPrice = $objectManager
            ->get('Magento\Framework\App\Config\ScopeConfigInterface')
            ->getValue(
                'tax/calculation/price_includes_tax',
                \Magento\Store\Model\ScopeInterface::SCOPE_STORE
            ) ?? false;
    }

    /**
     * @return mixed
     */
    public function getProducts()
    {
        $page = $this->request->getQueryValue('page', 1);
        $limit = $this->request->getQueryValue('limit', self::PER_PAGE);
        $this->searchCriteria
            ->addFilter('type_id', 'configurable', 'neq')
            ->setPageSize($limit)
            ->addFilter('store_id', $this->helperData->getGeneralConfig('store_id'))
            ->setCurrentPage($page);

        $list = $this->productRepository
            ->getList($this->searchCriteria->create());

        $products = [];

        foreach ($list->getItems() as $product) {
            $products[] = $this->productService->setProduct($product)->toArray();
        }

        return [[
            'perPage'  => $limit,
            'pages'    => ceil($list->getTotalCount() / $limit),
            'curPage'  => $page,
            'products' => $products,
        ]];
    }

    /**
     * @return mixed
     */
    public function getProduct()
    {
        $data = $this->request->getBodyParams();
        try {
            if (empty($data['product_id']) && empty($data['sku'])) {
                throw new \Exception("Missing product identifier");
            }

            if (!empty($data['product_id'])) {
                $product = $this->productRepository->getById($data['product_id'], true, 0, true);
            } else {
                $product = $this->productRepository->get($data['sku']);
            }

            return [[
                "product" => $this->productService->setProduct($product)->toArray(),
            ]];

        } catch (\Exception $exception) {
            return [[
                "success" => false,
                "message" => $exception->getMessage(),
            ]];
        }
    }

    /**
     * Optimized price endpoint.
     *
     * Uses a lean product collection (only price-relevant EAV attributes) instead of
     * productRepository::getList() which loads all attributes. The addFinalPrice() call
     * joins the pre-computed catalog_product_index_price table, avoiding on-the-fly
     * price rule calculations per product. Tax rates are cached per tax_class_id to
     * avoid repeated calculations for products sharing the same class.
     *
     * Supports ?updated_since=YYYY-MM-DD HH:MM:SS for incremental (delta) sync.
     *
     * @return mixed
     */
    public function getProductPrices()
    {
        $page = $this->request->getQueryValue('page', 1);
        $limit = $this->request->getQueryValue('limit', self::PER_PAGE);
        $updatedSince = $this->request->getQueryValue('updated_since');
        $storeId = $this->helperData->getGeneralConfig('store_id');

        /** @var \Magento\Catalog\Model\ResourceModel\Product\Collection $collection */
        $collection = $this->productCollectionFactory->create();
        $collection
            ->addAttributeToFilter('type_id', ['neq' => 'configurable'])
            ->addAttributeToSelect(['price', 'special_price', 'special_from_date', 'special_to_date', 'tax_class_id'])
            ->addFinalPrice()
            ->setStoreId($storeId)
            ->setOrder('entity_id', \Magento\Framework\Data\Collection::SORT_ORDER_ASC)
            ->setPageSize($limit)
            ->setCurPage($page);

        if ($updatedSince) {
            $collection->addAttributeToFilter('updated_at', ['gteq' => $updatedSince]);
        }

        $taxRateCache = [];
        $products = [];

        foreach ($collection->getItems() as $product) {
            $taxClassId = (int) $product->getData('tax_class_id');
            if (!array_key_exists($taxClassId, $taxRateCache)) {
                $taxRateCache[$taxClassId] = $this->taxCalculation->getCalculatedRate($taxClassId);
            }
            $taxRate = $taxRateCache[$taxClassId];

            if ($this->taxIncludedInPrice) {
                $salePrice = (float) $product->getFinalPrice();
                $salePrice = round($salePrice, EasySales::DECIMAL_PRECISION);

                $fullPrice = (float) $product->getPrice();
                $fullPrice = round($fullPrice, EasySales::DECIMAL_PRECISION);
            } else {
                $salePrice = (float) $product->getFinalPrice() * (1 + $taxRate / 100);
                $salePrice = round($salePrice, EasySales::DECIMAL_PRECISION);

                $fullPrice = (float) $product->getPrice() * (1 + $taxRate / 100);
                $fullPrice = round($fullPrice, EasySales::DECIMAL_PRECISION);
            }

            $products[] = [
                'product_website_id' => (int) $product->getId(),
                'sku'                => $product->getSku(),
                'prices'             => [
                    'sale_price' => $salePrice,
                    'full_price' => $fullPrice,
                    'tax_rate'   => $taxRate,
                ],
            ];
        }

        return [[
            'perPage' => $limit,
            'pages'   => ceil($collection->getSize() / $limit),
            'curPage' => $page,
            'prices'  => $products,
        ]];
    }

    /**
     * @return mixed
     */
    public function getProductStocks()
    {
        $page         = (int)$this->request->getQueryValue('page', 1);
        $limit        = (int)$this->request->getQueryValue('limit', self::PER_PAGE);
        $updatedSince = $this->request->getQueryValue('updated_since');

        $connection  = $this->resourceConnection->getConnection();
        $entityTable = $this->resourceConnection->getTableName('catalog_product_entity');

        $countSelect = $connection->select()
            ->from($entityTable, ['COUNT(*)'])
            ->where('type_id != ?', 'configurable');

        if ($updatedSince) {
            $countSelect->where('updated_at >= ?', $updatedSince);
        }

        $totalCount = (int)$connection->fetchOne($countSelect);

        if ($totalCount === 0) {
            return [[
                'perPage' => $limit,
                'pages'   => 0,
                'curPage' => $page,
                'stocks'  => [],
            ]];
        }

        $offset = ($page - 1) * $limit;

        $dataSelect = $connection->select()
            ->from($entityTable, ['entity_id', 'sku', 'type_id'])
            ->where('type_id != ?', 'configurable')
            ->order('entity_id ASC')
            ->limit($limit, $offset);

        if ($updatedSince) {
            $dataSelect->where('updated_at >= ?', $updatedSince);
        }

        $productRows = $connection->fetchAll($dataSelect);

        $skus      = [];
        $bundleIds = [];
        foreach ($productRows as $row) {
            if ($row['type_id'] === 'bundle') {
                $bundleIds[] = (int)$row['entity_id'];
            } else {
                $skus[] = $row['sku'];
            }
        }

        $stocksBySku  = $this->getStocksBulk($skus);
        $bundleStocks = $this->getBundleStocksBulk($bundleIds);

        $stocks = [];
        foreach ($productRows as $row) {
            if ($row['type_id'] === 'bundle') {
                $stock = $bundleStocks[(int)$row['entity_id']] ?? 0;
            } else {
                $stock = $stocksBySku[$row['sku']] ?? 0;
            }

            $stocks[] = [
                'product_website_id' => (int)$row['entity_id'],
                'sku'                => $row['sku'],
                'stock'              => $stock,
            ];
        }

        return [[
            'perPage' => $limit,
            'pages'   => ceil($totalCount / $limit),
            'curPage' => $page,
            'stocks'  => $stocks,
        ]];
    }

    /**
     * @param array $skus
     * @return array
     */
    private function getStocksBulk(array $skus): array
    {
        $stocksBySku = [];

        if (empty($skus)) {
            return $stocksBySku;
        }

        $connection = $this->resourceConnection->getConnection();
        $tableName  = $this->resourceConnection->getTableName('inventory_source_item');

        $select = $connection->select()
            ->from($tableName, ['sku', 'quantity', 'status'])
            ->where('sku IN (?)', $skus)
            ->where('source_code = ?', $this->defaultStockSource);

        foreach ($connection->fetchAll($select) as $row) {
            if ($row['status']) {
                $stocksBySku[$row['sku']] = (float)$row['quantity'];
            }
        }

        return $stocksBySku;
    }

    /**
     * @param array $bundleIds
     * @return array
     */
    private function getBundleStocksBulk(array $bundleIds): array
    {
        $bundleStocks = [];

        if (empty($bundleIds)) {
            return $bundleStocks;
        }

        $connection     = $this->resourceConnection->getConnection();
        $selectionTable = $this->resourceConnection->getTableName('catalog_product_bundle_selection');
        $productTable   = $this->resourceConnection->getTableName('catalog_product_entity');

        $select = $connection->select()
            ->from(['bs' => $selectionTable], ['parent_product_id', 'product_id'])
            ->join(['cpe' => $productTable], 'bs.product_id = cpe.entity_id', ['sku'])
            ->where('bs.parent_product_id IN (?)', $bundleIds);

        $bundleChildren = $connection->fetchAll($select);

        if (empty($bundleChildren)) {
            foreach ($bundleIds as $bundleId) {
                $bundleStocks[$bundleId] = 0;
            }
            return $bundleStocks;
        }

        $allChildSkus      = [];
        $bundleToChildSkus = [];
        foreach ($bundleChildren as $child) {
            $parentId                    = (int)$child['parent_product_id'];
            $sku                         = $child['sku'];
            $allChildSkus[]              = $sku;
            $bundleToChildSkus[$parentId][] = $sku;
        }

        $childStocks = $this->getStocksBulk(array_unique($allChildSkus));

        foreach ($bundleIds as $bundleId) {
            if (empty($bundleToChildSkus[$bundleId])) {
                $bundleStocks[$bundleId] = 0;
                continue;
            }

            $minStock = PHP_INT_MAX;
            foreach ($bundleToChildSkus[$bundleId] as $childSku) {
                $childStock = $childStocks[$childSku] ?? 0;
                if ($childStock < $minStock) {
                    $minStock = $childStock;
                }
            }

            $bundleStocks[$bundleId] = ($minStock === PHP_INT_MAX) ? 0 : (int)$minStock;
        }

        return $bundleStocks;
    }

    /**
     * @param string|null $productId
     * @return mixed|void
     * @throws \Magento\Framework\Exception\CouldNotSaveException
     * @throws \Magento\Framework\Exception\InputException
     * @throws \Magento\Framework\Exception\NoSuchEntityException
     */
    public function saveProduct(string $productId = null)
    {
        $data = $this->request->getBodyParams();
        try {
            /** @var \Magento\Catalog\Api\Data\ProductInterface|\Magento\Catalog\Model\Product $product */
            $product = $this->getNewOrExistingProduct($productId);

            // update stock after product save otherwise the new product quantity won't be reflected
            if ($this->sourceItemsBySku) {
                $stocks = $this->sourceItemsBySku->execute($product->getSku());

                $stockSourceItem = null;
                foreach ($stocks as $stock) {
                    if ($stock->getSourceCode() === $this->defaultStockSource) {
                        $stockSourceItem = $stock;
                        break;
                    }
                }

                // only update stock if there is a change
                if ($stockSourceItem && $stockSourceItem->getQuantity() !== $data['stock']) {
                    $stockSourceItem->setQuantity($data['stock']);
                    $stockSourceItem->save();

                    $objectManager = \Magento\Framework\App\ObjectManager::getInstance();
                    $stockRegistery = $objectManager->get('\Magento\CatalogInventory\Api\StockRegistryInterface');
                    $stockItem = $stockRegistery->getStockItem($product->getId());
                    $stockItem->setData('is_in_stock', $data['stock'] > 0 ? 1 : 0);
                    $stockItem->setData('qty', $data['stock']);
                    $stockItem->save();
                }
            }
        } catch (\Exception $exception) {
            return [[
                "success" => false,
                "message" => $exception->getMessage(),
            ]];
        }

        return [[
            "success" => true,
            "product" => $product->getId(),
            "stock"   => $data['stock'],
        ]];
    }

    /**
     * Get product by id or create a new product object
     *
     * @param $productId
     * @return \Magento\Catalog\Api\Data\ProductInterface|\Magento\Catalog\Model\Product
     * @throws \Magento\Framework\Exception\NoSuchEntityException
     */
    private function getNewOrExistingProduct($productId)
    {
        return $productId ? $this->productRepository->getById($productId, true, 0, true) : $this->productFactory->create();
    }
}
