<?php
declare(strict_types=1);

namespace Celena\Plugin\Shop\Support;

use Celena\Core\Database\Connection;
use Celena\Core\Security\Filter;

/**
 * Сопоставление текущей выборки витрины (категория + выбранные значения атрибутов)
 * с правилами SEO-фильтра из таблицы `shop_seo_filters` и подстановка переменных
 * в шаблоны H1/Title/Description/Keywords/Intro.
 *
 * Правило применяется, если:
 *   - category_id NULL ИЛИ совпадает с текущей категорией;
 *   - все условия (conditions) удовлетворены: для каждого {attr_slug, values}
 *     все указанные values входят в выбранные пользователем значения этого атрибута.
 * Сортировка: sort_order DESC, id DESC. Применяется первое подходящее.
 *
 * Поддерживаемые переменные:
 *   {category}       — название категории (или 'Магазин', если не выбрана)
 *   {attr_<slug>}    — выбранные значения атрибута через ", "
 *   {count}          — общее количество товаров (после фильтров)
 *   {price_min}      — нижняя граница цены (введённая пользователем или min категории)
 *   {price_max}      — верхняя граница цены
 *   {{               — литеральная фигурная скобка (escape)
 */
final class SeoFilterMatcher
{
    public function __construct(private readonly Connection $conn) {}

    /**
     * Найти и применить правило. Возвращает массив с готовыми (отрендеренными)
     * полями h1/title/description/keywords/intro_text — либо null, если правил нет.
     *
     * @param int|null $categoryId
     * @param array<int, array{attribute: array<string,mixed>, values: array<int,string>}> $selectedAttrs
     *   В формате AttributeFacets::parseRequest().
     * @param array<string, mixed> $context
     *   {category, count, price_min, price_max, ...} — значения для подстановки.
     * @return array{h1:string, meta_title:string, meta_description:string, meta_keywords:string, intro_text:string, rule_id:int}|null
     */
    public function match(?int $categoryId, array $selectedAttrs, array $context = []): ?array
    {
        $rule = $this->findRule($categoryId, $selectedAttrs);
        if ($rule === null) return null;
        return $this->renderRule($rule, $context);
    }

    /**
     * Найти правило с конкретным url_slug. Используется при роутинге ЧПУ.
     */
    public function findByUrlSlug(string $urlSlug): ?array
    {
        if ($urlSlug === '') return null;
        try {
            $row = $this->conn->builder('shop_seo_filters')
                ->where('is_active', '=', true)
                ->where('url_slug', '=', $urlSlug)
                ->first();
            return $row ?: null;
        } catch (\Throwable) {
            return null;
        }
    }

    /** Отрендерить страничные тексты заранее найденного правила. */
    public function applyRule(array $rule, array $context): array
    {
        return $this->renderRule($rule, $context);
    }

    /**
     * Поиск первого подходящего правила без рендера. Удобно для проверки, нужен ли 301-редирект.
     *
     * @return array<string, mixed>|null Сырая строка правила.
     */
    public function findRule(?int $categoryId, array $selectedAttrs): ?array
    {
        try {
            $rules = $this->conn->builder('shop_seo_filters')
                ->where('is_active', '=', true)
                ->orderBy('sort_order', 'desc')
                ->orderBy('id', 'desc')
                ->get();
        } catch (\Throwable) {
            return null;
        }
        if ($rules === []) return null;

        $selectedBySlug = [];
        foreach ($selectedAttrs as $info) {
            $slug = (string) $info['attribute']['slug'];
            $selectedBySlug[$slug] = array_flip(array_map('strval', $info['values']));
        }
        foreach ($rules as $rule) {
            $ruleCategoryId = $rule['category_id'] !== null ? (int) $rule['category_id'] : null;
            if ($ruleCategoryId !== null && $ruleCategoryId !== $categoryId) {
                continue;
            }
            if (!$this->conditionsMatch($rule['conditions'] ?? null, $selectedBySlug)) {
                continue;
            }
            return $rule;
        }
        return null;
    }

    /**
     * true, если выбранные атрибуты ТОЧНО совпадают с условиями правила
     * (ни одного лишнего значения и атрибута). Это критерий для 301-редиректа
     * на ЧПУ-URL — иначе при наличии «лишних» фильтров редирект бы потерял их.
     */
    public static function isExactMatch(array $rule, array $selectedAttrs): bool
    {
        $conds = is_array($rule['conditions'] ?? null) ? $rule['conditions'] : json_decode((string) ($rule['conditions'] ?? ''), true);
        if (!is_array($conds)) $conds = [];

        // Сравниваем мапы slug → отсортированный list of values.
        $expected = [];
        foreach ($conds as $c) {
            if (!is_array($c)) return false;
            $slug = (string) ($c['attr_slug'] ?? '');
            $vals = is_array($c['values'] ?? null) ? array_map('strval', $c['values']) : [];
            if ($slug === '' || $vals === []) return false;
            sort($vals);
            $expected[$slug] = $vals;
        }
        $actual = [];
        foreach ($selectedAttrs as $info) {
            $slug = (string) ($info['attribute']['slug'] ?? '');
            $vals = array_map('strval', $info['values']);
            sort($vals);
            $actual[$slug] = $vals;
        }
        ksort($expected);
        ksort($actual);
        return $expected === $actual;
    }

    /**
     * Развернуть условия правила в массив selectedAttrs формата AttributeFacets::parseRequest.
     * Используется при входе по ЧПУ для построения пайплайна как если бы пользователь
     * выбрал эти значения через сайдбар.
     *
     * @param array<string, mixed> $rule
     * @param array<int, array<string, mixed>> $filterableAttrs Все is_filterable атрибуты, как у AttributeFacets::loadFilterable.
     * @return array<int, array{attribute: array<string,mixed>, values: array<int,string>}>
     */
    public static function expandConditions(array $rule, array $filterableAttrs): array
    {
        $bySlug = [];
        foreach ($filterableAttrs as $a) $bySlug[(string) $a['slug']] = $a;

        $raw = $rule['conditions'] ?? null;
        $conds = is_array($raw) ? $raw : json_decode((string) $raw, true);
        if (!is_array($conds)) return [];

        $out = [];
        foreach ($conds as $c) {
            if (!is_array($c)) continue;
            $slug = (string) ($c['attr_slug'] ?? '');
            if (!isset($bySlug[$slug])) continue;
            $vals = is_array($c['values'] ?? null) ? array_values(array_unique(array_map('strval', $c['values']))) : [];
            if ($vals === []) continue;
            $attr = $bySlug[$slug];
            $out[(int) $attr['id']] = ['attribute' => $attr, 'values' => $vals];
        }
        return $out;
    }

    /**
     * Построить ЧПУ-URL правила. Возвращает '' если ЧПУ не настроен (нет url_slug
     * или категория правила пустая).
     */
    public static function buildUrl(array $rule, ?string $categorySlug): string
    {
        $urlSlug = trim((string) ($rule['url_slug'] ?? ''));
        if ($urlSlug === '' || $categorySlug === null || $categorySlug === '') return '';
        return '/shop/category/' . $categorySlug . '/' . $urlSlug;
    }

    /**
     * Сгенерировать предлагаемый url_slug из условий правила: транслит лейблов
     * через Filter::slug + префикс из имени атрибута. Уникальность не проверяется
     * (это делает контроллер при сохранении).
     */
    public static function suggestUrlSlug(array $conditions): string
    {
        if (!is_array($conditions) || $conditions === []) return '';
        $parts = [];
        foreach ($conditions as $c) {
            if (!is_array($c)) continue;
            $slug = trim((string) ($c['attr_slug'] ?? ''));
            $vals = is_array($c['values'] ?? null) ? $c['values'] : [];
            if ($slug === '' || $vals === []) continue;
            $valSlugs = [];
            foreach ($vals as $v) {
                $vs = Filter::slug((string) $v, 40);
                if ($vs !== '') $valSlugs[] = $vs;
            }
            if ($valSlugs === []) continue;
            $parts[] = $slug . '-' . implode('-', $valSlugs);
        }
        $out = implode('-', $parts);
        return Filter::urlSlug($out, 120);
    }

    /**
     * @param mixed $rawConditions JSON string | array | null
     * @param array<string, array<string,int>> $selectedBySlug
     */
    private function conditionsMatch(mixed $rawConditions, array $selectedBySlug): bool
    {
        $conds = is_array($rawConditions) ? $rawConditions : json_decode((string) $rawConditions, true);
        if (!is_array($conds) || $conds === []) {
            // Правило без условий = «всегда матчит» (только с фильтром по категории).
            return true;
        }
        foreach ($conds as $cond) {
            if (!is_array($cond)) return false;
            $slug = (string) ($cond['attr_slug'] ?? '');
            $values = $cond['values'] ?? [];
            if ($slug === '' || !is_array($values) || $values === []) return false;
            if (!isset($selectedBySlug[$slug])) return false;
            foreach ($values as $v) {
                if (!isset($selectedBySlug[$slug][(string) $v])) {
                    return false;
                }
            }
        }
        return true;
    }

    /**
     * @param array<string, mixed> $rule
     * @param array<string, mixed> $context
     * @return array{h1:string, meta_title:string, meta_description:string, meta_keywords:string, intro_text:string, rule_id:int}
     */
    private function renderRule(array $rule, array $context): array
    {
        $vars = self::buildVariables($context);
        return [
            'rule_id'          => (int) $rule['id'],
            'h1'               => self::render((string) ($rule['h1'] ?? ''), $vars),
            'meta_title'       => self::render((string) ($rule['meta_title'] ?? ''), $vars),
            'meta_description' => self::render((string) ($rule['meta_description'] ?? ''), $vars),
            'meta_keywords'    => self::render((string) ($rule['meta_keywords'] ?? ''), $vars),
            'intro_text'       => self::render((string) ($rule['intro_text'] ?? ''), $vars),
        ];
    }

    /**
     * Подготовить плоскую мапу плейсхолдер → значение из контекста.
     * Доступно: {category}, {count}, {price_min}, {price_max}, {attr_<slug>}.
     *
     * @param array<string, mixed> $context
     * @return array<string, string>
     */
    public static function buildVariables(array $context): array
    {
        $vars = [
            'category'  => (string) ($context['category'] ?? 'Магазин'),
            'count'     => (string) ($context['count'] ?? '0'),
            'price_min' => (string) ($context['price_min'] ?? ''),
            'price_max' => (string) ($context['price_max'] ?? ''),
        ];
        $selectedAttrs = $context['selected_attrs'] ?? [];
        if (is_array($selectedAttrs)) {
            foreach ($selectedAttrs as $info) {
                if (!is_array($info)) continue;
                $slug = (string) ($info['attribute']['slug'] ?? '');
                $values = $info['values'] ?? [];
                if ($slug === '' || !is_array($values)) continue;
                $vars['attr_' . $slug] = implode(', ', array_map('strval', $values));
            }
        }
        return $vars;
    }

    /**
     * Подставить переменные в шаблон. `{{` экранируется в `{`.
     *
     * @param array<string, string> $vars
     */
    public static function render(string $template, array $vars): string
    {
        if ($template === '') return '';
        // Защитим литеральные `{{` от обработки: временно заменим на маркер.
        $marker = "\x00LBRACE\x00";
        $template = str_replace('{{', $marker, $template);
        $out = preg_replace_callback(
            '/\{([a-zA-Z_][a-zA-Z0-9_-]*)\}/',
            static function ($m) use ($vars): string {
                $key = $m[1];
                return $vars[$key] ?? $m[0]; // неизвестные переменные оставляем как есть
            },
            $template
        ) ?? $template;
        return str_replace($marker, '{', $out);
    }
}
