<?php
declare(strict_types=1);

namespace Celena\Plugin\Shop\Support;

use Celena\Core\Database\Connection;
use Celena\Core\Database\QueryBuilder;

/**
 * Сборка фасетных фильтров витрины магазина по атрибутам товаров
 * (`/admin/shop/attributes`, флаг `is_filterable`).
 *
 * Multiselect хранится как JSON-массив строк в колонке value — для матчинга
 * используем POSITION('"value"' IN value) > 0, что одинаково работает в
 * PostgreSQL и MySQL без зависимости от driver-specific JSON-функций.
 */
final class AttributeFacets
{
    public const FACETED_TYPES = ['select', 'multiselect', 'color', 'boolean'];

    /**
     * Загрузить все атрибуты с is_filterable=true и подходящим type.
     *
     * @return array<int, array<string, mixed>>
     */
    public static function loadFilterable(Connection $conn): array
    {
        return $conn->builder('shop_attributes')
            ->where('is_filterable', '=', true)
            ->whereIn('type', self::FACETED_TYPES)
            ->orderBy('sort_order', 'asc')
            ->orderBy('title', 'asc')
            ->get();
    }

    /**
     * Распарсить $_GET['attr'] в нормализованный набор:
     *   [attribute_id => ['attribute' => row, 'values' => [v, v, ...]]].
     * Slug-и без атрибута и значения с не-фасетным типом отбрасываются.
     *
     * @param array<int, array<string, mixed>> $attributes Полный список фасетных атрибутов.
     * @param mixed $rawAttr Сырой $request->query('attr', []) — может быть не массивом.
     * @return array<int, array{attribute: array<string, mixed>, values: array<int, string>}>
     */
    public static function parseRequest(mixed $rawAttr, array $attributes): array
    {
        if (!is_array($rawAttr) || $rawAttr === []) {
            return [];
        }
        $bySlug = [];
        foreach ($attributes as $a) {
            $bySlug[(string) $a['slug']] = $a;
        }
        $out = [];
        foreach ($rawAttr as $slug => $values) {
            $slug = (string) $slug;
            if (!isset($bySlug[$slug])) {
                continue;
            }
            if (!is_array($values)) {
                $values = [$values];
            }
            $values = array_values(array_filter(array_map(
                static fn ($v): string => trim((string) $v),
                $values
            ), static fn (string $v): bool => $v !== ''));
            if ($values === []) {
                continue;
            }
            $attr = $bySlug[$slug];
            $type = (string) $attr['type'];
            if (!in_array($type, self::FACETED_TYPES, true)) {
                continue;
            }
            $out[(int) $attr['id']] = [
                'attribute' => $attr,
                'values'    => array_values(array_unique($values)),
            ];
        }
        return $out;
    }

    /**
     * Применить выбранные значения к QueryBuilder по shop_products.
     * Между разными атрибутами — AND, внутри одного — OR.
     *
     * @param array<int, array{attribute: array<string,mixed>, values: array<int,string>}> $selected
     */
    public static function apply(QueryBuilder $q, Connection $conn, array $selected): void
    {
        if ($selected === []) {
            return;
        }
        $av    = $conn->table_q('shop_attribute_values');
        $valC  = $conn->quoteId('value');
        $aidC  = $conn->quoteId('attribute_id');
        $pidC  = $conn->quoteId('product_id');
        $idC   = $conn->quoteId('id');

        foreach ($selected as $attrId => $info) {
            $type     = (string) $info['attribute']['type'];
            $values   = $info['values'];
            $clauses  = [];
            $bindings = [(int) $attrId];

            if ($type === 'multiselect') {
                foreach ($values as $v) {
                    $clauses[] = 'POSITION(? IN ' . $valC . ') > 0';
                    $bindings[] = '"' . $v . '"';
                }
            } elseif ($type === 'boolean') {
                // Чекбокс «Да» — value='1'. Поддерживаем legacy-варианты.
                $clauses[] = $valC . ' IN (?, ?, ?, ?)';
                array_push($bindings, '1', 'true', 'yes', 'on');
            } else {
                $placeholders = implode(',', array_fill(0, count($values), '?'));
                $clauses[] = $valC . ' IN (' . $placeholders . ')';
                foreach ($values as $v) {
                    $bindings[] = (string) $v;
                }
            }

            $sub = 'SELECT ' . $pidC . ' FROM ' . $av
                . ' WHERE ' . $aidC . ' = ?'
                . ' AND (' . implode(' OR ', $clauses) . ')';
            $q->whereRaw($idC . ' IN (' . $sub . ')', $bindings);
        }
    }

    /**
     * Построить структуру для сайдбара: для каждого фасетного атрибута —
     * список значений с counts по подмножеству $scopedProductIds.
     * Counts считаются по полному скоупу без учёта самого attr-фильтра
     * (стандартное поведение фасетов: видно «потенциал» добавления значения).
     *
     * @param array<int, array<string, mixed>> $filterable Результат loadFilterable().
     * @param array<int, int>                  $scopedProductIds Товары после фильтра категории/поиска/цены.
     * @param array<int, array{attribute: array<string,mixed>, values: array<int,string>}> $selected
     * @return array<int, array{slug:string,label:string,type:string,values:array<int,array{value:string,count:int,selected:bool}>,values_count:int}>
     */
    public static function buildFacets(Connection $conn, array $filterable, array $scopedProductIds, array $selected): array
    {
        if ($filterable === [] || $scopedProductIds === []) {
            return [];
        }

        $attrTypes   = [];
        $attrLabels  = [];
        $attrSlugs   = [];
        $attrIds     = [];
        $attrOptions = []; // [attrId => [label => {label,color,image}]]
        foreach ($filterable as $a) {
            $aid = (int) $a['id'];
            $attrIds[]            = $aid;
            $attrTypes[$aid]      = (string) $a['type'];
            $attrLabels[$aid]     = (string) $a['title'];
            $attrSlugs[$aid]      = (string) $a['slug'];
            $attrOptions[$aid]    = AttributeOptions::indexByLabel($a['options'] ?? null);
        }

        $av  = $conn->table_q('shop_attribute_values');
        $aid = $conn->quoteId('attribute_id');
        $pid = $conn->quoteId('product_id');
        $val = $conn->quoteId('value');

        $aidPh = implode(',', array_fill(0, count($attrIds), '?'));
        $pidPh = implode(',', array_fill(0, count($scopedProductIds), '?'));

        $sql = 'SELECT ' . $aid . ' AS attribute_id, ' . $val . ' AS value, '
            . 'COUNT(DISTINCT ' . $pid . ') AS cnt'
            . ' FROM ' . $av
            . ' WHERE ' . $aid . ' IN (' . $aidPh . ')'
            . ' AND ' . $pid . ' IN (' . $pidPh . ')'
            . ' GROUP BY ' . $aid . ', ' . $val;
        $rows = $conn->run($sql, array_merge($attrIds, $scopedProductIds))->fetchAll();

        // Аккумулируем counts: [attrId => [value => cnt]].
        $counts = [];
        foreach ($attrIds as $id) {
            $counts[$id] = [];
        }
        foreach ($rows as $r) {
            $aId  = (int) $r['attribute_id'];
            $type = $attrTypes[$aId] ?? 'text';
            $raw  = (string) ($r['value'] ?? '');
            $cnt  = (int) $r['cnt'];

            if ($type === 'multiselect') {
                $arr = json_decode($raw, true);
                if (!is_array($arr)) {
                    continue;
                }
                foreach ($arr as $v) {
                    $v = (string) $v;
                    if ($v === '') continue;
                    $counts[$aId][$v] = ($counts[$aId][$v] ?? 0) + $cnt;
                }
            } elseif ($type === 'boolean') {
                $isTrue = in_array($raw, ['1', 'true', 'yes', 'on'], true);
                if (!$isTrue) continue;
                $counts[$aId]['1'] = ($counts[$aId]['1'] ?? 0) + $cnt;
            } else {
                if ($raw === '') continue;
                $counts[$aId][$raw] = ($counts[$aId][$raw] ?? 0) + $cnt;
            }
        }

        // Карта выбранных значений для отметки selected.
        $selectedSet = []; // [attrId => [value => true]]
        foreach ($selected as $attrId => $info) {
            $selectedSet[(int) $attrId] = array_flip($info['values']);
        }

        $out = [];
        foreach ($filterable as $a) {
            $aId = (int) $a['id'];
            $vals = $counts[$aId] ?? [];
            if ($vals === []) continue;
            arsort($vals);
            $values = [];
            $optMap = $attrOptions[$aId] ?? [];
            foreach ($vals as $v => $c) {
                $vStr = (string) $v;
                $opt  = $optMap[$vStr] ?? null;
                $values[] = [
                    'value'    => $vStr,
                    'count'    => (int) $c,
                    'selected' => isset($selectedSet[$aId][$vStr]),
                    'color'    => $opt['color'] ?? null,
                    'image'    => $opt['image'] ?? null,
                ];
            }
            $out[] = [
                'slug'         => $attrSlugs[$aId],
                'label'        => $attrLabels[$aId],
                'type'         => $attrTypes[$aId],
                'values'       => $values,
                'values_count' => count($values),
            ];
        }
        return $out;
    }

    /**
     * Плоский список активных фильтров для chips под заголовком.
     * Шаблонизатор Celena теряет ключи при foreach по ассоциативному массиву.
     *
     * @param array<int, array{attribute: array<string,mixed>, values: array<int,string>}> $selected
     * @return array<int, array{slug:string,label:string,value:string,type:string}>
     */
    public static function flattenActive(array $selected): array
    {
        $out = [];
        foreach ($selected as $info) {
            $a = $info['attribute'];
            foreach ($info['values'] as $v) {
                $out[] = [
                    'slug'  => (string) $a['slug'],
                    'label' => (string) $a['title'],
                    'value' => (string) $v,
                    'type'  => (string) $a['type'],
                ];
            }
        }
        return $out;
    }

    /**
     * URL-параметр «attr» в виде, готовом для http_build_query.
     *
     * @param array<int, array{attribute: array<string,mixed>, values: array<int,string>}> $selected
     * @return array<string, array<int, string>>
     */
    public static function toUrlParam(array $selected): array
    {
        $out = [];
        foreach ($selected as $info) {
            $out[(string) $info['attribute']['slug']] = $info['values'];
        }
        return $out;
    }
}
