<?php

/**
 * VirtualStock Product Lookup Helper
 *
 * Matches VirtualStock order line items to backend Productsbe records by
 * supplier SKU / product name, and builds Select2-compatible option lists
 * for manual override in the import preview UI.
 *
 * @category    PortalPeople
 * @package     VirtualStock
 */

namespace PortalPeople\virtualstock\components;

use backend\models\Productsbe;
use Yii;

class ProductLookup
{
    /** @var array<string, Productsbe|null> */
    private static array $byIdCache = [];

    /** @var array<string, Productsbe|null> */
    private static array $bySkuCache = [];

    /** @var array<string, Productsbe|null> */
    private static array $byNameCache = [];

    /** @var array<string, array<int, Productsbe>> */
    private static array $candidateCache = [];

    /**
     * Add `product_lookup` metadata to each line in every order.
     *
     * @param array $orders Normalised order list from OrderImportService.
     * @return array
     */
    public static function enrichOrders(array $orders): array
    {
        foreach ($orders as $oi => $order) {
            $orders[$oi]['orderLines'] = self::enrichOrderLines($order['orderLines'] ?? []);
        }
        return $orders;
    }

    /**
     * Add `product_lookup` metadata to a flat array of order lines.
     *
     * @param array $lines
     * @return array
     */
    public static function enrichOrderLines(array $lines): array
    {
        foreach ($lines as $i => $line) {
            $lines[$i]['product_lookup'] = self::buildLookupData($line);
        }
        return $lines;
    }

    /**
     * Resolve the product to use during import.
     *
     * Priority:
     *  1. Manual override from the bulk-import UI (`$selectedProductId`)
     *  2. Exact SKU/stock-code match against `supplierProductNo` / `sku`
     *  3. Exact product-name match against `productDescription` / `name`
     *
     * @param array $line Normalised order line.
     * @param string|null $selectedProductId Manual override (product ID string).
     * @return Productsbe|null
     */
    public static function resolveProduct(array $line, ?string $selectedProductId = null): ?Productsbe
    {
        $selectedProductId = trim((string) $selectedProductId);
        if ($selectedProductId !== '') {
            $p = self::findById($selectedProductId);
            if ($p) {
                return $p;
            }
            Yii::warning("VirtualStock ProductLookup: manual override not found — {$selectedProductId}", 'virtualstock');
        }

        $sku = trim((string) ($line['supplierProductNo'] ?? $line['sku'] ?? ''));
        if ($sku !== '') {
            $p = self::findBySku($sku);
            if ($p) {
                return $p;
            }
        }

        $name = trim((string) ($line['productDescription'] ?? $line['name'] ?? ''));
        if ($name !== '') {
            $p = self::findByName($name);
            if ($p) {
                return $p;
            }
        }

        return null;
    }

    // -------------------------------------------------------------------------
    // Private helpers
    // -------------------------------------------------------------------------

    private static function buildLookupData(array $line): array
    {
        $sku  = trim((string) ($line['supplierProductNo'] ?? $line['sku'] ?? ''));
        $name = trim((string) ($line['productDescription'] ?? $line['name'] ?? ''));

        $matchedProduct = null;
        $matchType = 'none';

        if ($sku !== '') {
            $matchedProduct = self::findBySku($sku);
            if ($matchedProduct) {
                $matchType = 'sku';
            }
        }

        if (!$matchedProduct && $name !== '') {
            $matchedProduct = self::findByName($name);
            if ($matchedProduct) {
                $matchType = 'name';
            }
        }

        // Build dropdown options: first try a focused candidate search.
        $options = [];
        foreach (self::findCandidates($sku, $name) as $candidate) {
            $options[$candidate->id] = self::formatLabel($candidate);
        }

        // Ensure the auto-matched product always appears at the top.
        if ($matchedProduct && !isset($options[$matchedProduct->id])) {
            $options = [$matchedProduct->id => self::formatLabel($matchedProduct)] + $options;
        }

        // If no candidates, fall back to the full product list for manual search.
        $allProducts = false;
        if (empty($options)) {
            $allProducts = true;
            foreach (self::getAllProducts() as $p) {
                $options[$p->id] = self::formatLabel($p);
            }
        }

        return [
            'selectedProductId'   => $matchedProduct ? (string) $matchedProduct->id : '',
            'matchType'           => $matchType,
            'matchLabel'          => self::getMatchLabel($matchType),
            'matchedProductLabel' => $matchedProduct ? self::formatLabel($matchedProduct) : null,
            'options'             => $options,
            'allProducts'         => $allProducts,
        ];
    }

    private static function findCandidates(string $sku, string $name, int $limit = 15): array
    {
        $cacheKey = md5($sku . '|' . $name . '|' . $limit);
        if (array_key_exists($cacheKey, self::$candidateCache)) {
            return self::$candidateCache[$cacheKey];
        }

        $candidates = [];
        $seen = [];

        $add = static function (?Productsbe $p) use (&$candidates, &$seen): void {
            if (!$p || isset($seen[$p->id])) {
                return;
            }
            $seen[$p->id] = true;
            $candidates[] = $p;
        };

        if ($sku !== '') {
            $add(self::findBySku($sku));
        }

        if ($name !== '') {
            $add(self::findByName($name));

            $nameMatches = self::baseQuery()
                ->andWhere(['like', 'product_name', $name])
                ->orderBy(['product_name' => SORT_ASC, 'stock_code' => SORT_ASC])
                ->limit($limit)
                ->all();

            foreach ($nameMatches as $m) {
                $add($m);
            }
        }

        return self::$candidateCache[$cacheKey] = $candidates;
    }

    private static function getAllProducts(int $limit = 2000): array
    {
        return self::baseQuery()
            ->orderBy(['product_name' => SORT_ASC, 'stock_code' => SORT_ASC])
            ->limit($limit)
            ->all();
    }

    private static function findById(string $id): ?Productsbe
    {
        if (array_key_exists($id, self::$byIdCache)) {
            return self::$byIdCache[$id];
        }
        return self::$byIdCache[$id] = self::baseQuery()->andWhere(['id' => $id])->one();
    }

    private static function findBySku(string $sku): ?Productsbe
    {
        $k = strtolower($sku);
        if (array_key_exists($k, self::$bySkuCache)) {
            return self::$bySkuCache[$k];
        }
        $jsonStockcode = new yii\db\Expression('json_value(data, \'$.virtualstock.supplier_sku\')');
        $product = self::$bySkuCache[$k] = self::baseQuery()    
            ->andWhere(['stock_code' => $sku])
            ->one();
        if(!$product) {
            $product = self::$bySkuCache[$k] = self::baseQuery()
                ->andWhere(['=', $jsonStockcode, $sku])
                ->one();
        }

        return $product;
    }

    private static function findByName(string $name): ?Productsbe
    {
        $k = strtolower($name);
        if (array_key_exists($k, self::$byNameCache)) {
            return self::$byNameCache[$k];
        }
        return self::$byNameCache[$k] = self::baseQuery()->andWhere(['product_name' => $name])->one();
    }

    private static function baseQuery(): \yii\db\ActiveQuery
    {
        return Productsbe::find()->andWhere([
            'or',
            ['status' => null],
            ['not', ['status' => 'Deleted']],
        ]);
    }

    private static function formatLabel(Productsbe $p): string
    {
        $parts = array_filter([(string) $p->stock_code, (string) $p->product_name]);
        return implode(' — ', $parts);
    }

    private static function getMatchLabel(string $matchType): string
    {
        return match ($matchType) {
            'sku'  => 'SKU match',
            'name' => 'Name match',
            default => 'No match',
        };
    }
}
