<?php
declare(strict_types=1);

namespace Celena\Plugin\Shop\Controllers;

use Celena\Core\Database\Connection;
use Celena\Core\Http\Pagination;
use Celena\Core\Http\Request;
use Celena\Core\Http\Response;
use Celena\Core\Security\Filter;
use Celena\Core\Template\Engine;
use Celena\Module\CustomFields\FieldRepository;
use Celena\Module\Settings\Options;
use Celena\Plugin\Shop\Support\AttributeFacets;
use Celena\Plugin\Shop\Support\ProductSpecs;
use Celena\Plugin\Shop\Support\SeoFilterMatcher;

/**
 * Публичная витрина магазина.
 */
final class ShopController
{
    private const SORTS = [
        'newest'     => ['column' => 'id',    'direction' => 'desc'],
        'cheap'      => ['column' => 'price', 'direction' => 'asc'],
        'expensive'  => ['column' => 'price', 'direction' => 'desc'],
        'name'       => ['column' => 'title', 'direction' => 'asc'],
        'stock'      => ['column' => 'stock', 'direction' => 'desc'],
    ];

    public function __construct(
        private readonly Engine $engine,
        private readonly Connection $conn,
        private readonly Options $options,
    ) {
    }

    public function index(Request $request, array $params = []): Response
    {
        $page = Filter::int($request->query('page', 1), 1);
        $perPage = 24;
        $q = $this->conn->builder('shop_products')->where('is_active', '=', true);

        // ── Категория ────────────────────────────────────────────────
        // ЧПУ: /shop/category/{slug}. Старый /shop?category={slug} 301-редиректим
        // на новый URL, сохраняя остальные query-параметры (поиск/сортировка/attr).
        $pathSlug  = Filter::urlSlug($params['slug'] ?? '');
        $querySlug = Filter::urlSlug($request->query('category', ''));
        if ($pathSlug === '' && $querySlug !== '') {
            $qs = $_GET;
            unset($qs['category']);
            $target = '/shop/category/' . $querySlug;
            if ($qs !== []) $target .= '?' . http_build_query($qs);
            return Response::redirect($target, 301);
        }
        $categorySlug = $pathSlug !== '' ? $pathSlug : '';
        $categoryRow = null;
        if ($categorySlug !== '') {
            $categoryRow = $this->conn->builder('celena_categories')
                ->where('entity_type', '=', 'shop_product')
                ->where('slug', '=', $categorySlug)->first();
            if ($categoryRow === null) {
                return Response::notFound();
            }
            $q->where('category_id', '=', (int) $categoryRow['id']);
        }

        // ── Поиск ────────────────────────────────────────────────────
        $search = Filter::string($request->query('q', ''), 200);
        if ($search !== '') {
            $pattern = '%' . mb_strtolower($search, 'UTF-8') . '%';
            $q->whereRaw('LOWER(' . $this->conn->quoteId('title') . ') LIKE ?', [$pattern]);
        }

        // ── Фильтр по цене ───────────────────────────────────────────
        $priceMin = (float) $request->query('price_min', 0);
        $priceMax = (float) $request->query('price_max', 0);
        if ($priceMin > 0) {
            $q->where('price', '>=', $priceMin);
        }
        if ($priceMax > 0) {
            $q->where('price', '<=', $priceMax);
        }

        // ── Фильтры по атрибутам товара ──────────────────────────────
        // Источник:
        //   • ЧПУ /shop/category/{cat}/{seo_slug} — условия разворачиваются из правила;
        //   • иначе query ?attr[color][]=Червоний&attr[material][]=Шкіра.
        // Атрибуты с is_filterable=true и type ∈ {select,multiselect,color,boolean}
        // автоматически попадают в сайдбар (см. AttributeFacets::FACETED_TYPES).
        $filterableAttrs = AttributeFacets::loadFilterable($this->conn);
        $matcher         = new SeoFilterMatcher($this->conn);
        $seoSlug         = isset($params['seo_slug']) ? (string) $params['seo_slug'] : '';
        $seoRule         = null;

        if ($seoSlug !== '') {
            // ЧПУ-маршрут: ищем правило по url_slug, валидируем категорию,
            // разворачиваем его условия в selectedAttrs.
            $seoRule = $matcher->findByUrlSlug($seoSlug);
            if ($seoRule === null) {
                return Response::notFound();
            }
            $ruleCatId = $seoRule['category_id'] !== null ? (int) $seoRule['category_id'] : null;
            $curCatId  = $categoryRow !== null ? (int) $categoryRow['id'] : null;
            if ($ruleCatId !== null && $ruleCatId !== $curCatId) {
                return Response::notFound();
            }
            $selectedAttrs = SeoFilterMatcher::expandConditions($seoRule, $filterableAttrs);
        } else {
            $selectedAttrs = AttributeFacets::parseRequest($request->query('attr', []), $filterableAttrs);

            // Точный матч query-фильтров с правилом, у которого есть url_slug → 301 на ЧПУ.
            if ($selectedAttrs !== [] && $categorySlug !== '') {
                $exactRule = $matcher->findRule($categoryRow !== null ? (int) $categoryRow['id'] : null, $selectedAttrs);
                if ($exactRule !== null
                    && !empty($exactRule['url_slug'])
                    && SeoFilterMatcher::isExactMatch($exactRule, $selectedAttrs)
                ) {
                    $qs = $_GET;
                    unset($qs['attr'], $qs['category']);
                    $target = SeoFilterMatcher::buildUrl($exactRule, $categorySlug);
                    if ($qs !== []) $target .= '?' . http_build_query($qs);
                    return Response::redirect($target, 301);
                }
            }
        }

        // Клон builder ДО применения attr-фильтра — для корректных counts фасетов
        // (counts по полному скоупу категории/поиска/цены, без учёта самих attr).
        $scopeForFacets = clone $q;
        AttributeFacets::apply($q, $this->conn, $selectedAttrs);

        // ── Пагинация (COUNT) — ДО добавления ORDER BY,
        //    иначе PostgreSQL ругается на column not in GROUP BY. ───
        $total = (clone $q)->count();
        $p = new Pagination($page, $perPage, $total);

        // ── Сортировка ───────────────────────────────────────────────
        $sortKey = (string) Filter::in($request->query('sort', 'newest'), array_keys(self::SORTS), 'newest');
        $sortDef = self::SORTS[$sortKey];
        $q->orderBy($sortDef['column'], $sortDef['direction']);

        // ── Выборка ──────────────────────────────────────────────────
        $products = $q->limit($p->perPage)->offset($p->offset)->get();

        // Подгрузить custom-field значения для всех товаров одним запросом —
        // нужно для блока «Характеристики» в карточке товара (ProductSpecs).
        $productIds  = array_map(static fn ($r): int => (int) $r['id'], $products);
        $cfRepo      = new FieldRepository($this->conn);
        $cfValuesMap = $cfRepo->valuesForMany('shop_product', $productIds);
        $fieldByLabel = ProductSpecs::preloadCfFields($cfRepo);

        foreach ($products as &$pr) {
            $pr['url'] = '/shop/product/' . $pr['slug'];
            $pr['price_fmt'] = number_format((float) $pr['price'], 2, '.', ' ');
            $pr['old_price_fmt'] = !empty($pr['old_price'])
                ? number_format((float) $pr['old_price'], 2, '.', ' ')
                : '';

            $pr['specs'] = ProductSpecs::extract(
                $pr,
                $cfValuesMap[(int) $pr['id']] ?? [],
                $fieldByLabel,
                $this->conn
            );
            // Сериализованные options/variations для data-* атрибутов карточки.
            $pr['options_json']    = is_string($pr['options'] ?? null)    ? (string) $pr['options']    : (string) json_encode($pr['options'] ?? null);
            $pr['variations_json'] = is_string($pr['variations'] ?? null) ? (string) $pr['variations'] : (string) json_encode($pr['variations'] ?? null);
            $pr['has_variations']  = !empty($pr['options']) && !empty($pr['variations']) ? 1 : 0;
        }
        unset($pr);

        $categories = $this->conn->builder('celena_categories')
            ->where('entity_type', '=', 'shop_product')
            ->orderBy('sort_order', 'asc')->orderBy('title', 'asc')
            ->get();

        // ── Доступные значения атрибутов (для сайдбара фасетов) ──────
        // Counts считаются по полному скоупу (категория/поиск/цена) без учёта
        // выбранных attr-фильтров — фасеты показывают «потенциал».
        $scopeIdsRows     = (clone $scopeForFacets)->select('id')->get();
        $scopeProductIds  = array_map(static fn ($r): int => (int) $r['id'], $scopeIdsRows);
        $attrFilters      = AttributeFacets::buildFacets(
            $this->conn,
            $filterableAttrs,
            $scopeProductIds,
            $selectedAttrs
        );

        // ── Диапазон цен в текущей категории ─────────────────────────
        $priceRange = $this->priceRange($categoryRow !== null ? (int) $categoryRow['id'] : 0);

        $this->engine->setTheme((string) $this->options->get('theme', 'default'));

        // ── SEO-фильтр (конструктор подмены H1/мета-тегов) ──────────
        // В ЧПУ-режиме правило уже найдено по url_slug (выше) — переиспользуем его.
        // В query-режиме матчер сам найдёт подходящее правило (может быть без url_slug).
        $seoContext = [
            'category'       => $categoryRow !== null ? (string) $categoryRow['title'] : 'Магазин',
            'count'          => $total,
            'price_min'      => $priceMin > 0 ? (string) $priceMin : (string) $priceRange['min'],
            'price_max'      => $priceMax > 0 ? (string) $priceMax : (string) $priceRange['max'],
            'selected_attrs' => $selectedAttrs,
        ];
        $seo = $seoRule !== null
            ? $matcher->applyRule($seoRule, $seoContext)
            : $matcher->match(
                $categoryRow !== null ? (int) $categoryRow['id'] : null,
                $selectedAttrs,
                $seoContext
            );

        // Если матчер нашёл правило (для query-режима) — запомним его для canonical/url
        // построения ниже. В ЧПУ-режиме activeRule = $seoRule.
        $activeRule = $seoRule;
        if ($activeRule === null && $seo !== null) {
            // Контейнер-результат render не несёт исходную строку — не критично,
            // canonical и URL-base нам нужны только если есть url_slug. Загрузим повторно.
            $activeRule = $matcher->findRule(
                $categoryRow !== null ? (int) $categoryRow['id'] : null,
                $selectedAttrs
            );
        }

        $defaultTitle    = $categoryRow !== null ? (string) $categoryRow['title'] : 'Магазин';
        $defaultDesc     = $categoryRow !== null ? (string) ($categoryRow['description'] ?? '') : '';
        // SEO-overrides: пустая строка = нет правила, шаблон использует свой fallback.
        $seoH1           = ($seo !== null) ? (string) $seo['h1']               : '';
        $seoMetaTitle    = ($seo !== null) ? (string) $seo['meta_title']       : '';
        $seoMetaDesc     = ($seo !== null) ? (string) $seo['meta_description'] : '';
        $seoMetaKeywords = ($seo !== null) ? (string) $seo['meta_keywords']    : '';
        $seoIntro        = ($seo !== null) ? (string) $seo['intro_text']       : '';
        $pageTitle       = $seoH1        !== '' ? $seoH1        : $defaultTitle;
        $pageSeoTitle    = $seoMetaTitle !== '' ? $seoMetaTitle : $defaultTitle;
        $pageSeoDesc     = $seoMetaDesc  !== '' ? $seoMetaDesc  : $defaultDesc;

        $activeAttrParam = AttributeFacets::toUrlParam($selectedAttrs);
        // База URL.
        // • ЧПУ-режим: /shop/category/{cat}/{seo_slug} — attr НЕ передаются (они в правиле).
        // • Иначе:    /shop/category/{cat} либо /shop — attr идут в query.
        $isSeoUrl = ($activeRule !== null) && !empty($activeRule['url_slug']) && $categorySlug !== '';
        if ($isSeoUrl) {
            $base = SeoFilterMatcher::buildUrl($activeRule, $categorySlug);
            $qsParams = array_filter([
                'q'         => $search !== '' ? $search : null,
                'sort'      => $sortKey !== 'newest' ? $sortKey : null,
                'price_min' => $priceMin > 0 ? $priceMin : null,
                'price_max' => $priceMax > 0 ? $priceMax : null,
            ]);
        } else {
            $base = $categorySlug !== '' ? '/shop/category/' . $categorySlug : '/shop';
            $qsParams = array_filter([
                'q'         => $search !== '' ? $search : null,
                'sort'      => $sortKey !== 'newest' ? $sortKey : null,
                'price_min' => $priceMin > 0 ? $priceMin : null,
                'price_max' => $priceMax > 0 ? $priceMax : null,
                'attr'      => $activeAttrParam !== [] ? $activeAttrParam : null,
            ]);
        }
        $qs = http_build_query($qsParams);
        $urlPattern = $base . '?' . ($qs === '' ? 'page=:page' : ($qs . '&page=:page'));

        // Канонический URL — без пагинации, для исключения дублей в индексе.
        $canonicalUrl = $base . ($qs !== '' ? '?' . $qs : '');

        $html = $this->engine->render('plugins/shop/front/index', [
            'site' => ['name' => (string) $this->options->get('site_name', '')],
            'page' => [
                'title' => $pageTitle,
                'seo_title' => $pageSeoTitle,
                'seo_description' => $pageSeoDesc,
                'seo_keywords' => $seoMetaKeywords,
                'canonical' => $canonicalUrl,
                'og_image' => '',
            ],
            'seo_h1'    => $seoH1,
            'seo_intro' => $seoIntro,
            'products' => $products,
            'categories' => $categories,
            'current_category' => $categoryRow,
            'search' => $search,
            'pagination' => $p->toArray($urlPattern),
            'sort' => $sortKey,
            'sort_options' => array_keys(self::SORTS),
            'price_range' => $priceRange,
            'price_min' => $priceMin,
            'price_max' => $priceMax,
            'attr_filters' => $attrFilters,
            'active_attr'  => AttributeFacets::flattenActive($selectedAttrs),
            'total' => $total,
        ]);
        return Response::html($html);
    }

    public function product(Request $request, array $params): Response
    {
        $slug = Filter::urlSlug($params['slug'] ?? '');
        $row = $this->conn->builder('shop_products')->where('slug', '=', $slug)->where('is_active', '=', true)->first();
        if ($row === null) {
            return Response::notFound();
        }
        // Счётчик просмотров. Не считаем хиты от админов и ботов
        // (User-Agent — простая эвристика). Атомарный UPDATE, без race condition.
        try {
            $ua = strtolower((string) ($_SERVER['HTTP_USER_AGENT'] ?? ''));
            $isBot = $ua === '' || preg_match('/(bot|crawl|spider|slurp|fetch|preview|monitor)/', $ua) === 1;
            if (!$isBot) {
                $tbl = $this->conn->table_q('shop_products');
                $col = $this->conn->quoteId('views');
                $idCol = $this->conn->quoteId('id');
                $this->conn->run("UPDATE $tbl SET $col = $col + 1 WHERE $idCol = ?", [(int) $row['id']]);
                $row['views'] = (int) ($row['views'] ?? 0) + 1;
            }
        } catch (\Throwable) {
        }
        $category = null;
        if (!empty($row['category_id'])) {
            $category = $this->conn->builder('celena_categories')->where('entity_type', '=', 'shop_product')->where('id', '=', (int) $row['category_id'])->first();
        }
        $row['url'] = '/shop/product/' . (string) $row['slug'];
        $row['price_fmt'] = number_format((float) $row['price'], 2, '.', ' ');
        $row['old_price_fmt'] = $row['old_price'] !== null ? number_format((float) $row['old_price'], 2, '.', ' ') : '';

        // Подгрузка characteristics: cf + options + attributes (через helper).
        $cfRepo = new FieldRepository($this->conn);
        $cfValues = $cfRepo->valuesFor('shop_product', (int) $row['id']);
        $fieldByLabel = ProductSpecs::preloadCfFields($cfRepo);
        $row['specs'] = ProductSpecs::extract($row, $cfValues, $fieldByLabel, $this->conn);

        // Сериализация options/variations для data-* и JS-вариаций.
        $row['options_json']    = is_string($row['options'] ?? null)
            ? (string) $row['options']
            : (string) json_encode($row['options'] ?? null);
        $row['variations_json'] = is_string($row['variations'] ?? null)
            ? (string) $row['variations']
            : (string) json_encode($row['variations'] ?? null);
        $row['has_variations']  = !empty($row['options']) && !empty($row['variations']) ? '1' : '';

        // Галерея: cover + изображения из колонки images (JSON).
        $gallery = [];
        if (!empty($row['cover'])) {
            $gallery[] = (string) $row['cover'];
        }
        $imagesRaw = $row['images'] ?? null;
        if ($imagesRaw !== null && $imagesRaw !== '') {
            $images = is_string($imagesRaw) ? json_decode($imagesRaw, true) : $imagesRaw;
            if (is_array($images)) {
                foreach ($images as $img) {
                    $img = (string) $img;
                    if ($img !== '' && !in_array($img, $gallery, true)) {
                        $gallery[] = $img;
                    }
                }
            }
        }
        $row['gallery'] = $gallery;

        // Отзывы о товаре — последние одобренные + сводный рейтинг.
        // Обёрнуто в try/catch: если миграция shop_reviews ещё не применена,
        // страница товара продолжает работать (отзывов просто не будет).
        $reviewRows   = [];
        $reviewsCount = 0;
        $reviewsAvg   = 0.0;
        $reviewBarsList = [
            ['stars' => 5, 'count' => 0, 'percent' => 0],
            ['stars' => 4, 'count' => 0, 'percent' => 0],
            ['stars' => 3, 'count' => 0, 'percent' => 0],
            ['stars' => 2, 'count' => 0, 'percent' => 0],
            ['stars' => 1, 'count' => 0, 'percent' => 0],
        ];
        $reviewsList = [];
        try {
            $reviewRows = $this->conn->builder('shop_reviews')
                ->where('product_id', '=', (int) $row['id'])
                ->where('is_approved', '=', true)
                ->orderBy('created_at', 'desc')
                ->limit(50)
                ->get();
            $reviewsCount = count($reviewRows);
            if ($reviewsCount > 0) {
                $sum = 0;
                foreach ($reviewRows as $rv) $sum += (int) $rv['rating'];
                $reviewsAvg = round($sum / $reviewsCount, 1);
            }
            $reviewsBars = [5 => 0, 4 => 0, 3 => 0, 2 => 0, 1 => 0];
            foreach ($reviewRows as $rv) {
                $r = max(1, min(5, (int) $rv['rating']));
                $reviewsBars[$r] = ($reviewsBars[$r] ?? 0) + 1;
            }
            $reviewBarsList = [];
            foreach ($reviewsBars as $stars => $cnt) {
                $reviewBarsList[] = [
                    'stars'   => $stars,
                    'count'   => $cnt,
                    'percent' => $reviewsCount > 0 ? (int) round($cnt / $reviewsCount * 100) : 0,
                ];
            }
            foreach ($reviewRows as $rv) {
                $authorName = (string) ($rv['author_name'] ?? '');
                $reviewsList[] = [
                    'id'             => (int) $rv['id'],
                    'author_name'    => $authorName,
                    'author_initial' => $authorName !== '' ? mb_strtoupper(mb_substr($authorName, 0, 1, 'UTF-8'), 'UTF-8') : '',
                    'rating'         => (int) ($rv['rating'] ?? 0),
                    'content'        => (string) ($rv['content'] ?? ''),
                    'date'           => (string) ($rv['created_at'] ?? ''),
                ];
            }
        } catch (\Throwable $e) {
            // Таблицы пока нет — это нормально до применения миграции.
        }

        // Похожие товары — той же категории, без текущего, до 10 шт.
        $related = [];
        if (!empty($row['category_id'])) {
            $rq = $this->conn->builder('shop_products')
                ->where('is_active', '=', true)
                ->where('category_id', '=', (int) $row['category_id'])
                ->where('id', '!=', (int) $row['id'])
                ->orderBy('id', 'desc')
                ->limit(10);
            foreach ($rq->get() as $rp) {
                $rp['url'] = '/shop/product/' . (string) $rp['slug'];
                $rp['price_fmt'] = number_format((float) $rp['price'], 2, '.', ' ');
                $rp['old_price_fmt'] = !empty($rp['old_price'])
                    ? number_format((float) $rp['old_price'], 2, '.', ' ') : '';
                $related[] = $rp;
            }
        }

        $this->engine->setTheme((string) $this->options->get('theme', 'default'));
        $html = $this->engine->render('plugins/shop/front/product', [
            'site' => ['name' => (string) $this->options->get('site_name', '')],
            'page' => [
                'title' => (string) $row['title'],
                'seo_title' => (string) ($row['seo_title'] ?? $row['title']),
                'seo_description' => (string) ($row['seo_description'] ?? $row['short'] ?? ''),
                'og_image' => (string) ($row['cover'] ?? ''),
            ],
            'product' => $row,
            'category' => $category,
            'related' => $related,
            'reviews' => [
                'count'  => $reviewsCount,
                'avg'    => $reviewsAvg,
                'list'   => $reviewsList,
                'bars'   => $reviewBarsList,
            ],
        ]);
        return Response::html($html);
    }

    /**
     * Минимум/максимум цены товаров текущей категории.
     * @return array{min:int,max:int}
     */
    private function priceRange(int $categoryId): array
    {
        $sql = 'SELECT MIN(price) AS pmin, MAX(price) AS pmax FROM '
            . $this->conn->table_q('shop_products') . ' WHERE is_active = ?';
        $params = [true];
        if ($categoryId > 0) {
            $sql .= ' AND category_id = ?';
            $params[] = $categoryId;
        }
        $row = $this->conn->run($sql, $params)->fetch();
        $min = (int) floor((float) ($row['pmin'] ?? 0));
        $max = (int) ceil((float) ($row['pmax'] ?? 0));
        if ($max <= $min) $max = $min + 1;
        return ['min' => $min, 'max' => $max];
    }
}
