<?php
declare(strict_types=1);

namespace Celena\Plugin\Shop\Admin;

use Celena\Core\Database\Connection;
use Celena\Core\Http\Pagination;
use Celena\Core\Http\Request;
use Celena\Core\Http\Response;
use Celena\Core\Security\Auth;
use Celena\Core\Security\Csrf;
use Celena\Core\Security\Filter;
use Celena\Core\Template\Engine;
use Celena\Module\AdminBase\AdminController;
use Celena\Module\Settings\Options;
use Celena\Plugin\Shop\Support\AttributeOptions;

final class ProductsAdminController extends AdminController
{
    private function tr(): \Celena\Core\I18n\Translator
    {
        return \Celena\Core\Kernel::container()->make(\Celena\Core\I18n\Translator::class);
    }

    public function __construct(
        Engine $engine,
        Auth $auth,
        Options $options,
        private readonly Connection $conn,
    ) {
        parent::__construct($engine, $auth, $options);
    }

    public function index(Request $request): Response
    {
        if (!$this->auth->check()) {
            return Response::redirect('/admin/login');
        }

        $perPageAllowed = [10, 25, 50, 100];
        $perPage = Filter::int($request->query('per_page', 25), 25);
        if (!in_array($perPage, $perPageAllowed, true)) {
            $perPage = 25;
        }
        $page = Filter::int($request->query('page', 1), 1);

        $tab = (string) $request->query('tab', 'all');
        if (!in_array($tab, ['all', 'active', 'draft', 'archived'], true)) {
            $tab = 'all';
        }
        $search        = trim((string) $request->query('q', ''));
        $categoryId    = Filter::int($request->query('category', 0), 0);
        $manufacturerId = Filter::int($request->query('manufacturer', 0), 0);
        $sort          = (string) $request->query('sort', 'updated_desc');
        $stock         = (string) $request->query('stock', 'any');
        $priceMin      = $request->query('price_min', '');
        $priceMax      = $request->query('price_max', '');
        $featured      = (string) $request->query('featured', '0') === '1';
        $noImage       = (string) $request->query('no_image', '0') === '1';

        $sortMap = [
            'updated_desc' => ['updated_at', 'desc'],
            'updated_asc'  => ['updated_at', 'asc'],
            'price_desc'   => ['price', 'desc'],
            'price_asc'    => ['price', 'asc'],
            'title_asc'    => ['title', 'asc'],
            'stock_asc'    => ['stock', 'asc'],
            'stock_desc'   => ['stock', 'desc'],
            'rating_desc'  => ['rating_avg', 'desc'],
        ];
        if (!isset($sortMap[$sort])) {
            $sort = 'updated_desc';
        }

        $applyFilters = function ($q) use ($tab, $search, $categoryId, $manufacturerId, $stock, $priceMin, $priceMax, $featured, $noImage) {
            if ($tab === 'active')   $q->where('status', '=', 'active');
            if ($tab === 'draft')    $q->where('status', '=', 'draft');
            if ($tab === 'archived') $q->where('status', '=', 'archived');

            if ($search !== '') {
                $like = '%' . mb_strtolower($search, 'UTF-8') . '%';
                if (ctype_digit($search)) {
                    $q->whereRaw(
                        '(' . $this->conn->quoteId('id') . ' = ? OR LOWER(' . $this->conn->quoteId('title') . ') LIKE ? OR LOWER(' . $this->conn->quoteId('sku') . ') LIKE ? OR LOWER(' . $this->conn->quoteId('slug') . ') LIKE ?)',
                        [(int) $search, $like, $like, $like]
                    );
                } else {
                    $q->whereRaw(
                        '(LOWER(' . $this->conn->quoteId('title') . ') LIKE ? OR LOWER(' . $this->conn->quoteId('sku') . ') LIKE ? OR LOWER(' . $this->conn->quoteId('slug') . ') LIKE ?)',
                        [$like, $like, $like]
                    );
                }
            }
            if ($categoryId > 0)     $q->where('category_id', '=', $categoryId);
            if ($manufacturerId > 0) $q->where('manufacturer_id', '=', $manufacturerId);

            if ($stock === 'in')        $q->where('stock', '>', 0);
            if ($stock === 'out')       $q->where('stock', '=', 0);
            if ($stock === 'low')       $q->whereRaw($this->conn->quoteId('stock') . ' BETWEEN ? AND ?', [1, 5]);

            if ($priceMin !== '' && is_numeric($priceMin)) $q->where('price', '>=', (float) $priceMin);
            if ($priceMax !== '' && is_numeric($priceMax)) $q->where('price', '<=', (float) $priceMax);

            if ($featured) $q->where('is_featured', '=', true);
            if ($noImage)  $q->whereRaw('(' . $this->conn->quoteId('cover') . ' IS NULL OR ' . $this->conn->quoteId('cover') . " = '')");
        };

        // Счётчики табов считаются один раз — без фильтров (по полю status).
        $countAll      = $this->conn->builder('shop_products')->count();
        $countActive   = $this->conn->builder('shop_products')->where('status', '=', 'active')->count();
        $countDraft    = $this->conn->builder('shop_products')->where('status', '=', 'draft')->count();
        $countArchived = $this->conn->builder('shop_products')->where('status', '=', 'archived')->count();

        $countQ = $this->conn->builder('shop_products');
        $applyFilters($countQ);
        $total = $countQ->count();

        $p = new Pagination($page, $perPage, $total);

        $listQ = $this->conn->builder('shop_products');
        $applyFilters($listQ);
        [$col, $dir] = $sortMap[$sort];
        $rows = $listQ
            ->orderBy($col, $dir)
            ->orderBy('id', 'desc')
            ->limit($p->perPage)->offset($p->offset)
            ->get();

        // Подгружаем категории и производителей (для лейблов в строках и фильтров).
        $categoriesAll = $this->conn->builder('celena_categories')
            ->where('entity_type', '=', 'shop_product')
            ->orderBy('title', 'asc')
            ->get();
        $catMap = [];
        foreach ($categoriesAll as $c) {
            $catMap[(int) $c['id']] = (string) $c['title'];
        }

        $manufacturers = [];
        try {
            $manufacturers = $this->conn->builder('shop_manufacturers')
                ->orderBy('title', 'asc')
                ->get();
        } catch (\Throwable) {
        }
        $manMap = [];
        foreach ($manufacturers as $m) {
            $manMap[(int) $m['id']] = (string) $m['title'];
        }

        // Подмешиваем в строки текстовые лейблы и считаем дополнительные изображения.
        foreach ($rows as &$r) {
            $r['category_title']     = isset($r['category_id']) ? ($catMap[(int) $r['category_id']] ?? '') : '';
            $r['manufacturer_title'] = isset($r['manufacturer_id']) ? ($manMap[(int) $r['manufacturer_id']] ?? '') : '';
            $imagesArr = [];
            if (!empty($r['images'])) {
                $decoded = is_array($r['images']) ? $r['images'] : json_decode((string) $r['images'], true);
                if (is_array($decoded)) $imagesArr = $decoded;
            }
            $r['extra_images_count'] = max(0, count($imagesArr));
            $r['discount_pct']       = 0;
            if (!empty($r['old_price']) && (float) $r['old_price'] > 0 && (float) $r['price'] > 0) {
                $r['discount_pct'] = (int) round(100 - ((float) $r['price'] / (float) $r['old_price']) * 100);
            }
        }
        unset($r);

        // Соберём строку query для пагинации (без page).
        $qsParts = [];
        foreach (['tab', 'q', 'category', 'manufacturer', 'sort', 'per_page', 'stock', 'price_min', 'price_max', 'featured', 'no_image'] as $k) {
            $v = $request->query($k, '');
            if ($v !== '' && $v !== null) {
                $qsParts[] = rawurlencode($k) . '=' . rawurlencode((string) $v);
            }
        }
        $qs = $qsParts === [] ? '' : ('&' . implode('&', $qsParts));

        return $this->view('plugins/shop/admin/products_index', [
            'products'      => $rows,
            'pagination'    => $p->toArray('/admin/shop/products?page=:page' . $qs),
            'tab'           => $tab,
            'tab_counts'    => ['all' => $countAll, 'active' => $countActive, 'draft' => $countDraft, 'archived' => $countArchived],
            'categories'    => $categoriesAll,
            'manufacturers' => $manufacturers,
            'filters'       => [
                'q' => $search, 'category' => $categoryId, 'manufacturer' => $manufacturerId,
                'sort' => $sort, 'per_page' => $perPage,
                'stock' => $stock, 'price_min' => $priceMin, 'price_max' => $priceMax,
                'featured' => $featured, 'no_image' => $noImage,
            ],
            'has_active_extra_filters' => ($stock !== 'any' || $priceMin !== '' || $priceMax !== '' || $featured || $noImage),
            'total' => $total,
        ], 'shop', $this->tr()->translate('shop.products_list.title'));
    }

    /** Переключение «избранного» (звёздочка). */
    public function toggleFeatured(Request $request, array $params): Response
    {
        if (!$this->auth->check()) return Response::redirect('/admin/login');
        if (!Csrf::validate((string) $request->input('_csrf'))) {
            return $this->redirect('/admin/shop/products', $this->tr()->translate('shop.flash.csrf'), 'error');
        }
        $id = Filter::int($params['id'] ?? 0);
        $row = $this->conn->builder('shop_products')->where('id', '=', $id)->first();
        if ($row === null) return $this->redirect('/admin/shop/products', $this->tr()->translate('shop.flash.not_found'), 'error');
        $next = empty($row['is_featured']);
        $this->conn->builder('shop_products')->where('id', '=', $id)->update([
            'is_featured' => $next,
            'updated_at' => date('Y-m-d H:i:s'),
        ]);
        $back = (string) $request->input('back', '/admin/shop/products');
        return Response::redirect($back);
    }

    public function create(Request $request): Response
    {
        if (!$this->auth->check()) return Response::redirect('/admin/login');
        $categories = $this->conn->builder('celena_categories')
            ->where('entity_type', '=', 'shop_product')
            ->orderBy('sort_order', 'asc')->orderBy('title', 'asc')
            ->get();
        $repo = new \Celena\Module\CustomFields\FieldRepository($this->conn);
        $fields = \Celena\Module\CustomFields\FieldRenderer::prepare($repo->fieldsFor('shop_product'), []);
        $manufacturers = [];
        try { $manufacturers = $this->conn->builder('shop_manufacturers')->orderBy('title', 'asc')->get(); } catch (\Throwable) {}
        $attributes = $this->loadAttributes(0);
        return $this->view('plugins/shop/admin/products_form', [
            'row' => $this->emptyRow(),
            'categories' => $categories,
            'manufacturers' => $manufacturers,
            'attributes' => $attributes,
            'custom_fields' => $fields,
            'form_action' => '/admin/shop/products/create',
        ], 'shop', $this->tr()->translate('shop.product.new'));
    }

    public function store(Request $request): Response
    {
        if (!$this->auth->check()) return Response::redirect('/admin/login');
        if (!Csrf::validate((string) $request->input('_csrf'))) {
            return $this->redirect('/admin/shop/products/create', $this->tr()->translate('shop.flash.csrf'), 'error');
        }
        $data = $this->extract($request);
        if ($data['title'] === '') {
            return $this->redirect('/admin/shop/products/create', $this->tr()->translate('shop.flash.title_required'), 'error');
        }
        if ($data['slug'] === '') {
            $data['slug'] = Filter::slug($data['title']);
        }
        if ($this->slugExists($data['slug'])) {
            return $this->redirect('/admin/shop/products/create', $this->tr()->translate('shop.flash.slug_taken_already'), 'error');
        }
        $now = date('Y-m-d H:i:s');
        $data['created_at'] = $now;
        $data['updated_at'] = $now;
        $id = (int) $this->conn->builder('shop_products')->insert($data);
        $repo = new \Celena\Module\CustomFields\FieldRepository($this->conn);
        $repo->saveValues('shop_product', $id, (array) $request->input('cf', []));
        $this->saveAttributeValues($id, (array) $request->input('attr', []));
        return $this->redirect('/admin/shop/products', $this->tr()->translate('shop.flash.product_added'));
    }

    public function edit(Request $request, array $params): Response
    {
        if (!$this->auth->check()) return Response::redirect('/admin/login');
        $id = Filter::int($params['id'] ?? 0);
        $row = $this->conn->builder('shop_products')->where('id', '=', $id)->first();
        if ($row === null) {
            return $this->redirect('/admin/shop/products', $this->tr()->translate('shop.flash.not_found'), 'error');
        }
        $categories = $this->conn->builder('celena_categories')
            ->where('entity_type', '=', 'shop_product')
            ->orderBy('sort_order', 'asc')->orderBy('title', 'asc')
            ->get();
        $repo = new \Celena\Module\CustomFields\FieldRepository($this->conn);
        $values = $repo->valuesFor('shop_product', (int) $row['id']);
        $fields = \Celena\Module\CustomFields\FieldRenderer::prepare($repo->fieldsFor('shop_product'), $values);
        $manufacturers = [];
        try { $manufacturers = $this->conn->builder('shop_manufacturers')->orderBy('title', 'asc')->get(); } catch (\Throwable) {}
        $attributes = $this->loadAttributes((int) $row['id']);
        return $this->view('plugins/shop/admin/products_form', [
            'row' => $row,
            'categories' => $categories,
            'manufacturers' => $manufacturers,
            'attributes' => $attributes,
            'custom_fields' => $fields,
            'form_action' => '/admin/shop/products/' . $id . '/update',
        ], 'shop', 'Товар: ' . $row['title']);
    }

    public function update(Request $request, array $params): Response
    {
        if (!$this->auth->check()) return Response::redirect('/admin/login');
        if (!Csrf::validate((string) $request->input('_csrf'))) {
            return $this->redirect('/admin/shop/products', $this->tr()->translate('shop.flash.csrf'), 'error');
        }
        $id = Filter::int($params['id'] ?? 0);
        $existing = $this->conn->builder('shop_products')->where('id', '=', $id)->first();
        if ($existing === null) return $this->redirect('/admin/shop/products', $this->tr()->translate('shop.flash.not_found'), 'error');
        $data = $this->extract($request);
        if ($data['slug'] === '') $data['slug'] = Filter::slug($data['title']);
        if ($data['slug'] !== $existing['slug'] && $this->slugExists($data['slug'])) {
            return $this->redirect('/admin/shop/products/' . $id . '/edit', $this->tr()->translate('shop.flash.slug_taken'), 'error');
        }
        $data['updated_at'] = date('Y-m-d H:i:s');
        $this->conn->builder('shop_products')->where('id', '=', $id)->update($data);
        $repo = new \Celena\Module\CustomFields\FieldRepository($this->conn);
        $repo->saveValues('shop_product', $id, (array) $request->input('cf', []));
        $this->saveAttributeValues($id, (array) $request->input('attr', []));
        return $this->redirect('/admin/shop/products', $this->tr()->translate('shop.flash.saved'));
    }

    public function delete(Request $request, array $params): Response
    {
        if (!$this->auth->check()) return Response::redirect('/admin/login');
        if (!Csrf::validate((string) $request->input('_csrf'))) {
            return $this->redirect('/admin/shop/products', $this->tr()->translate('shop.flash.csrf'), 'error');
        }
        $id = Filter::int($params['id'] ?? 0);
        $this->conn->builder('shop_products')->where('id', '=', $id)->delete();
        // Чистим значения доп. полей и атрибутов, привязанные к удалённому товару.
        $this->conn->builder('celena_custom_field_values')
            ->where('entity_type', '=', 'shop_product')
            ->where('entity_id', '=', $id)
            ->delete();
        try { $this->conn->builder('shop_attribute_values')->where('product_id', '=', $id)->delete(); } catch (\Throwable) {}
        return $this->redirect('/admin/shop/products', $this->tr()->translate('shop.flash.deleted'));
    }

    private function emptyRow(): array
    {
        return [
            'id' => 0, 'slug' => '', 'title' => '', 'short' => '', 'body' => '',
            'price' => 0, 'old_price' => '', 'sku' => '', 'barcode' => '', 'stock' => 0,
            'is_active' => true, 'is_featured' => false, 'status' => 'active',
            'category_id' => '', 'manufacturer_id' => '',
            'cover' => '', 'images' => '',
            'options' => '', 'variations' => '',
            'seo_title' => '', 'seo_description' => '',
            'rating_avg' => 0, 'rating_count' => 0,
        ];
    }

    private function extract(Request $request): array
    {
        $catRaw = $request->input('category_id');
        $cat = ($catRaw === '' || $catRaw === null) ? null : Filter::int($catRaw);
        $manRaw = $request->input('manufacturer_id');
        $man = ($manRaw === '' || $manRaw === null) ? null : Filter::int($manRaw);
        $oldRaw = (string) $request->input('old_price', '');
        $status = (string) $request->input('status', 'active');
        if (!in_array($status, ['active', 'draft', 'archived'], true)) {
            $status = 'active';
        }
        // Галерея — единый список; обложка = первое фото. Это инвариант новой формы:
        // JS синхронизирует cover при любом изменении, но на сервере дублируем,
        // чтобы пост из старого клиента или с отключённым JS не сломал данные.
        $imagesNormalized = $this->normalizeImages($request->input('images'));
        $imagesList = $imagesNormalized !== null ? (json_decode($imagesNormalized, true) ?: []) : [];
        $coverFromForm = Filter::string($request->input('cover'), 500);
        $cover = $imagesList !== [] ? (string) $imagesList[0] : $coverFromForm;

        return [
            'title' => Filter::string($request->input('title'), 255),
            'slug' => Filter::urlSlug($request->input('slug', '')),
            'short' => Filter::string($request->input('short'), 2000),
            'body' => Filter::string($request->input('body'), 65535 * 4),
            'price' => (float) $request->input('price', 0),
            'old_price' => $oldRaw === '' ? null : (float) $oldRaw,
            'sku' => Filter::string($request->input('sku'), 64),
            'barcode' => Filter::string($request->input('barcode'), 64),
            'stock' => Filter::int($request->input('stock'), 0),
            'status' => $status,
            'is_active' => $status === 'active',
            'is_featured' => Filter::bool($request->input('is_featured')),
            'category_id' => $cat === 0 ? null : $cat,
            'manufacturer_id' => $man === 0 ? null : $man,
            'cover' => $cover,
            'images' => $imagesNormalized,
            'options' => $this->normalizeOptions($request->input('options_json')),
            'variations' => $this->normalizeVariations($request->input('variations_json')),
            'seo_title' => Filter::string($request->input('seo_title'), 255),
            'seo_description' => Filter::string($request->input('seo_description'), 65535),
        ];
    }

    /**
     * Чистим JSON опций: ожидаем массив [{ name: string, values: string[] }, …].
     * Любые лишние поля удаляются.
     */
    private function normalizeOptions(mixed $raw): ?string
    {
        if (!is_string($raw) || $raw === '' || $raw === 'null') return null;
        $decoded = json_decode($raw, true);
        if (!is_array($decoded)) return null;
        $clean = [];
        foreach ($decoded as $opt) {
            if (!is_array($opt)) continue;
            $name = trim((string) ($opt['name'] ?? ''));
            $values = $opt['values'] ?? [];
            if ($name === '' || !is_array($values)) continue;
            $cleanValues = [];
            foreach ($values as $v) {
                $v = trim((string) $v);
                if ($v !== '' && mb_strlen($v) <= 100) $cleanValues[] = $v;
            }
            if ($cleanValues === []) continue;
            $clean[] = [
                'name'   => mb_substr($name, 0, 80),
                'values' => array_values(array_unique($cleanValues)),
            ];
        }
        return $clean === [] ? null : json_encode($clean, JSON_UNESCAPED_UNICODE);
    }

    /**
     * Чистим JSON вариаций: каждая — { id, values[], price, old_price, stock, sku, image }.
     */
    private function normalizeVariations(mixed $raw): ?string
    {
        if (!is_string($raw) || $raw === '' || $raw === 'null') return null;
        $decoded = json_decode($raw, true);
        if (!is_array($decoded)) return null;
        $clean = [];
        $nextId = 1;
        foreach ($decoded as $v) {
            if (!is_array($v)) continue;
            $values = $v['values'] ?? [];
            if (!is_array($values) || $values === []) continue;
            $clean[] = [
                'id'        => isset($v['id']) ? (int) $v['id'] : $nextId,
                'values'    => array_values(array_map(fn ($x) => mb_substr(trim((string) $x), 0, 100), $values)),
                'price'     => (float) ($v['price'] ?? 0),
                'old_price' => isset($v['old_price']) && $v['old_price'] !== '' && $v['old_price'] !== null ? (float) $v['old_price'] : 0,
                'stock'     => (int) ($v['stock'] ?? 0),
                'sku'       => mb_substr(trim((string) ($v['sku'] ?? '')), 0, 64),
                'image'     => mb_substr(trim((string) ($v['image'] ?? '')), 0, 500),
            ];
            $nextId++;
        }
        return $clean === [] ? null : json_encode($clean, JSON_UNESCAPED_UNICODE);
    }

    private function normalizeImages(mixed $raw): ?string
    {
        if (!is_string($raw) || $raw === '' || $raw === 'null') {
            return null;
        }
        $decoded = json_decode($raw, true);
        if (!is_array($decoded)) {
            return null;
        }
        $clean = [];
        foreach ($decoded as $url) {
            if (is_string($url) && $url !== '' && strlen($url) < 500) {
                $clean[] = $url;
            }
        }
        return $clean === [] ? null : json_encode(array_values($clean), JSON_UNESCAPED_UNICODE);
    }

    private function slugExists(string $slug): bool
    {
        return $this->conn->builder('shop_products')->where('slug', '=', $slug)->first() !== null;
    }

    /**
     * Загружает все атрибуты, при наличии productId — подставляет существующие значения.
     * options для select/multiselect/color декодируются из JSON в массив.
     * Для multiselect значение хранится как JSON-строка списка.
     *
     * @return array<int, array<string, mixed>>
     */
    private function loadAttributes(int $productId): array
    {
        try {
            $attrs = $this->conn->builder('shop_attributes')
                ->orderBy('sort_order', 'asc')->orderBy('title', 'asc')->get();
        } catch (\Throwable) {
            return [];
        }
        $values = [];
        if ($productId > 0) {
            try {
                $rows = $this->conn->builder('shop_attribute_values')
                    ->where('product_id', '=', $productId)->get();
                foreach ($rows as $r) $values[(int) $r['attribute_id']] = (string) ($r['value'] ?? '');
            } catch (\Throwable) {}
        }
        foreach ($attrs as &$a) {
            // Унифицированный формат [{label,color,image},...] — совместим со старыми
            // массивами строк (через AttributeOptions::decode).
            $opts = AttributeOptions::decode($a['options'] ?? null);
            $val = $values[(int) $a['id']] ?? '';
            $a['value'] = $val;
            $valueArr = [];
            if ($a['type'] === 'multiselect' && $val !== '') {
                $decoded = json_decode($val, true);
                $valueArr = is_array($decoded) ? array_map('strval', $decoded) : [];
            }
            // options_decorated: [{ value, color, image, selected }, ...]
            // Шаблонизатор не умеет проверять элемент в массиве, поэтому делаем здесь.
            $a['options_decorated'] = [];
            foreach ($opts as $opt) {
                $label = (string) $opt['label'];
                $a['options_decorated'][] = [
                    'value'    => $label,
                    'color'    => $opt['color'],
                    'image'    => $opt['image'],
                    'selected' => $a['type'] === 'multiselect'
                        ? in_array($label, $valueArr, true)
                        : ($val === $label),
                ];
            }
            $a['checked'] = ($a['type'] === 'boolean') && in_array($val, ['1', 'true', 'yes', 'on'], true);
        }
        unset($a);
        return $attrs;
    }

    /**
     * Сохраняет значения атрибутов — простой UPSERT через delete+insert
     * (атрибутов мало, оптимизировать не нужно).
     */
    private function saveAttributeValues(int $productId, array $posted): void
    {
        try {
            $this->conn->builder('shop_attribute_values')->where('product_id', '=', $productId)->delete();
            foreach ($posted as $attrId => $value) {
                $attrId = (int) $attrId;
                if ($attrId <= 0) continue;
                if (is_array($value)) {
                    $value = array_values(array_filter(array_map('strval', $value), fn ($v) => $v !== ''));
                    if ($value === []) continue;
                    $stored = json_encode($value, JSON_UNESCAPED_UNICODE);
                } else {
                    $stored = trim((string) $value);
                    if ($stored === '') continue;
                }
                $this->conn->builder('shop_attribute_values')->insert([
                    'product_id'   => $productId,
                    'attribute_id' => $attrId,
                    'value'        => $stored,
                ]);
            }
        } catch (\Throwable) {
        }
    }
}
