<?php
declare(strict_types=1);

namespace Celena\Plugin\Shop\Sources;

use Celena\Core\Database\Connection;
use Celena\Module\CustomFields\FieldRepository;
use Celena\Plugin\Shop\Support\ProductSpecs;

/**
 * Источник для блок-тега [products ...]. Возвращает список товаров с фильтрами:
 *   - category="slug" или category="slug1,slug2" или category="3" (id)
 *   - limit="N" — сколько (по умолчанию 12, максимум 100)
 *   - offset="N"
 *   - sort="id|price|title|created_at|updated_at|stock" — поле сортировки
 *   - order="asc|desc"
 *   - ids="1,5,7" — точечный список (перебивает category/sort)
 *   - include_inactive="1" — включить скрытые товары (по умолчанию только is_active)
 *
 * Для каждого товара добавляются готовые поля:
 *   - url             — /shop/product/{slug}
 *   - price_fmt       — отформатированная цена «1 234.50»
 *   - old_price_fmt   — старая цена (если задана)
 *   - specs           — список характеристик [{label, value}] (cf + options + attributes, до 4 пар)
 *   - options_json    — сериализованные опции вариаций (для data-product-options в шаблоне)
 *   - variations_json — сериализованные комбинации (для data-product-variations)
 *   - has_variations  — '1' если есть и options, и variations; иначе ''
 */
final class ProductsContentSource
{
    private const ALLOWED_SORT = ['id', 'price', 'title', 'created_at', 'updated_at', 'stock'];

    public function __construct(
        private readonly Connection $conn,
        private readonly ?FieldRepository $cfRepo = null,
    ) {
    }

    /**
     * @param array<string, mixed> $params
     * @return array<int, array<string, mixed>>
     */
    public function fetch(array $params): array
    {
        $q = $this->conn->builder('shop_products');

        if (!empty($params['ids'])) {
            $ids = $this->parseIds((string) $params['ids']);
            if ($ids === []) return [];
            $q->whereIn('id', $ids);
        } else {
            // Активность
            $includeInactive = !empty($params['include_inactive']);
            if (!$includeInactive) {
                $q->where('is_active', '=', true);
            }
            // Категория
            if (!empty($params['category'])) {
                $catIds = $this->resolveCategoryIds((string) $params['category']);
                if ($catIds === []) return [];
                $q->whereIn('category_id', $catIds);
            }
        }

        $sort = (string) ($params['sort'] ?? 'id');
        if (!in_array($sort, self::ALLOWED_SORT, true)) $sort = 'id';
        $order = strtolower((string) ($params['order'] ?? 'desc')) === 'asc' ? 'asc' : 'desc';
        $q->orderBy($sort, $order);

        $limit = (int) ($params['limit'] ?? 12);
        if ($limit < 1) $limit = 12;
        if ($limit > 100) $limit = 100;
        $q->limit($limit);
        $offset = (int) ($params['offset'] ?? 0);
        if ($offset > 0) $q->offset($offset);

        $rows = $q->get();
        if ($rows === []) return $rows;

        // Подгрузить custom-field значения одним запросом для всех товаров.
        $cfValuesMap = [];
        $fieldByLabel = [];
        if ($this->cfRepo !== null) {
            $ids = array_map(static fn ($r): int => (int) $r['id'], $rows);
            $cfValuesMap  = $this->cfRepo->valuesForMany('shop_product', $ids);
            $fieldByLabel = ProductSpecs::preloadCfFields($this->cfRepo);
        }

        foreach ($rows as &$r) {
            $r['url']           = '/shop/product/' . (string) ($r['slug'] ?? '');
            $r['price_fmt']     = number_format((float) ($r['price'] ?? 0), 2, '.', ' ');
            $r['old_price_fmt'] = !empty($r['old_price'])
                ? number_format((float) $r['old_price'], 2, '.', ' ')
                : '';
            $r['specs'] = ProductSpecs::extract(
                $r,
                $cfValuesMap[(int) $r['id']] ?? [],
                $fieldByLabel,
                $this->conn
            );
            // Сериализованные options/variations для data-* атрибутов карточки
            // (используются JS для рендеринга selectов и расчёта вариации).
            $r['options_json']    = $this->jsonString($r['options'] ?? null);
            $r['variations_json'] = $this->jsonString($r['variations'] ?? null);
            $r['has_variations']  = !empty($r['options']) && !empty($r['variations']) ? '1' : '';
        }
        unset($r);
        return $rows;
    }

    private function jsonString(mixed $value): string
    {
        if ($value === null || $value === '') return '';
        if (is_string($value)) return $value;
        $encoded = json_encode($value);
        return $encoded === false ? '' : $encoded;
    }

    /** @return int[] */
    private function parseIds(string $raw): array
    {
        $out = [];
        foreach (explode(',', $raw) as $piece) {
            $n = (int) trim($piece);
            if ($n > 0) $out[] = $n;
        }
        return array_values(array_unique($out));
    }

    /** @return int[] */
    private function resolveCategoryIds(string $raw): array
    {
        $tokens = array_filter(array_map('trim', explode(',', $raw)), fn ($t) => $t !== '');
        if ($tokens === []) return [];
        $idsByNum = [];
        $slugs = [];
        foreach ($tokens as $t) {
            if (ctype_digit($t)) $idsByNum[] = (int) $t;
            else $slugs[] = $t;
        }
        $found = $idsByNum;
        if ($slugs !== []) {
            // Категории магазина живут в общей таблице celena_categories
            // под entity_type='shop_product'.
            $rows = $this->conn->builder('celena_categories')
                ->where('entity_type', '=', 'shop_product')
                ->whereIn('slug', $slugs)
                ->get();
            foreach ($rows as $r) $found[] = (int) $r['id'];
        }
        return array_values(array_unique($found));
    }
}
