<?php
declare(strict_types=1);

namespace Celena\Module\Categories;

use Celena\Core\Database\Connection;
use Celena\Core\Http\Request;
use Celena\Core\Http\Response;
use Celena\Core\Plugin\Hook;
use Celena\Core\Security\Auth;
use Celena\Core\Security\Csrf;
use Celena\Core\Security\Filter;
use Celena\Core\Security\Permission;
use Celena\Core\Template\Engine;
use Celena\Module\AdminBase\AdminController;
use Celena\Module\Settings\Options;

final class CategoriesController extends AdminController
{
    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 (!Permission::can($this->auth->user(), 'categories.read')) {
            return $this->redirect('/admin', 'Доступ запрещён.', 'error');
        }
        $entities = $this->entityTypes();
        $current = (string) $request->query('entity', '');
        if ($current !== '' && !$this->isValidEntity($current, $entities)) {
            $current = '';
        }
        $q = $this->conn->builder('celena_categories')
            ->orderBy('entity_type', 'asc')->orderBy('sort_order', 'asc')->orderBy('title', 'asc');
        if ($current !== '') {
            $q->where('entity_type', '=', $current);
        }
        $rows = $q->get();
        // Лейблы сущностей в строках для удобства
        $labels = [];
        foreach ($entities as $e) $labels[$e['slug']] = $e['label'];
        $tree = $this->flattenTree($rows);
        foreach ($tree as &$r) {
            $r['entity_label'] = $labels[$r['entity_type'] ?? 'news'] ?? (string) ($r['entity_type'] ?? 'news');
        }
        unset($r);
        return $this->view('admin/categories/index', [
            'categories'     => $tree,
            'entities'       => $entities,
            'current_entity' => $current,
        ], 'categories', 'Категории');
    }

    public function create(Request $request): Response
    {
        $entities = $this->entityTypes();
        $defaultEntity = $entities[0]['slug'] ?? 'news';
        // Можно прийти с /admin/categories/create?entity=shop_product — заранее выбрать
        $preselected = (string) $request->query('entity', $defaultEntity);
        if (!$this->isValidEntity($preselected, $entities)) $preselected = $defaultEntity;
        // Родители — только из той же сущности (категория одной сущности не может быть родителем другой).
        $all = $this->conn->builder('celena_categories')
            ->where('entity_type', '=', $preselected)
            ->orderBy('sort_order', 'asc')->orderBy('title', 'asc')->get();
        $parents = $this->flattenTree($all);
        return $this->view('admin/categories/form', [
            'row' => [
                'id' => 0, 'slug' => '', 'title' => '', 'description' => '',
                'parent_id' => '', 'sort_order' => 0, 'seo_title' => '', 'seo_description' => '',
                'entity_type' => $preselected,
            ],
            'parents' => $parents,
            'entities' => $entities,
            'form_action' => '/admin/categories/create',
        ], 'categories', 'Новая категория');
    }

    public function store(Request $request): Response
    {
        if (!Csrf::validate((string) $request->input('_csrf'))) {
            return $this->redirect('/admin/categories/create', 'CSRF.', 'error');
        }
        $data = $this->extract($request);
        if ($data['title'] === '') {
            return $this->redirect('/admin/categories/create', 'Название обязательно.', 'error');
        }
        if ($data['slug'] === '') {
            $data['slug'] = Filter::slug($data['title']);
        }
        if ($this->slugExists($data['slug'])) {
            return $this->redirect('/admin/categories/create', 'Такой slug уже существует.', 'error');
        }
        $now = date('Y-m-d H:i:s');
        $this->conn->builder('celena_categories')->insert($data + ['created_at' => $now, 'updated_at' => $now]);
        return $this->redirect('/admin/categories', 'Категория создана.');
    }

    public function edit(Request $request, array $params): Response
    {
        $id = Filter::int($params['id'] ?? 0);
        $row = $this->conn->builder('celena_categories')->where('id', '=', $id)->first();
        if ($row === null) {
            return $this->redirect('/admin/categories', 'Не найдена.', 'error');
        }
        $entities = $this->entityTypes();
        // Родители — только в рамках той же сущности.
        $all = $this->conn->builder('celena_categories')
            ->where('entity_type', '=', (string) ($row['entity_type'] ?? 'news'))
            ->orderBy('sort_order', 'asc')->orderBy('title', 'asc')->get();
        // Исключаем саму категорию и её потомков — чтобы нельзя было сделать цикл.
        $descendants = $this->collectDescendants($all, $id);
        $descendants[$id] = true;
        $parents = $this->flattenTree(array_values(array_filter($all, fn ($c) => !isset($descendants[(int) $c['id']]))));
        return $this->view('admin/categories/form', [
            'row' => $row,
            'parents' => $parents,
            'entities' => $entities,
            'form_action' => '/admin/categories/' . $id . '/update',
        ], 'categories', 'Редактировать: ' . $row['title']);
    }

    public function update(Request $request, array $params): Response
    {
        if (!Csrf::validate((string) $request->input('_csrf'))) {
            return $this->redirect('/admin/categories', 'CSRF.', 'error');
        }
        $id = Filter::int($params['id'] ?? 0);
        $existing = $this->conn->builder('celena_categories')->where('id', '=', $id)->first();
        if ($existing === null) {
            return $this->redirect('/admin/categories', 'Не найдена.', '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/categories/' . $id . '/edit', 'Slug уже занят.', 'error');
        }
        $data['updated_at'] = date('Y-m-d H:i:s');
        $this->conn->builder('celena_categories')->where('id', '=', $id)->update($data);
        return $this->redirect('/admin/categories', 'Сохранено.');
    }

    public function delete(Request $request, array $params): Response
    {
        if (!Csrf::validate((string) $request->input('_csrf'))) {
            return $this->redirect('/admin/categories', 'CSRF.', 'error');
        }
        $id = Filter::int($params['id'] ?? 0);
        $cat = $this->conn->builder('celena_categories')->where('id', '=', $id)->first();
        if ($cat === null) {
            return $this->redirect('/admin/categories', 'Не найдена.', 'error');
        }
        $entityType = (string) ($cat['entity_type'] ?? 'news');
        // Отвязать новости (только если это была news-категория)
        if ($entityType === 'news') {
            $this->conn->builder('celena_news')->where('category_id', '=', $id)->update(['category_id' => null]);
        }
        // Открепить дочерние
        $this->conn->builder('celena_categories')->where('parent_id', '=', $id)->update(['parent_id' => null]);
        $this->conn->builder('celena_categories')->where('id', '=', $id)->delete();
        // Уведомляем плагины, чтобы они отвязали свои сущности (например, shop_products.category_id).
        Hook::action('category.deleted', $cat);
        return $this->redirect('/admin/categories', 'Категория удалена.');
    }

    private function extract(Request $request): array
    {
        $parentRaw = $request->input('parent_id');
        $parent = ($parentRaw === '' || $parentRaw === null) ? null : Filter::int($parentRaw);
        $entities = $this->entityTypes();
        $entityType = (string) $request->input('entity_type', 'news');
        if (!$this->isValidEntity($entityType, $entities)) {
            $entityType = $entities[0]['slug'] ?? 'news';
        }
        return [
            'title' => Filter::string($request->input('title'), 255),
            'slug' => Filter::urlSlug($request->input('slug', '')),
            'description' => Filter::string($request->input('description'), 65535),
            'parent_id' => $parent === 0 ? null : $parent,
            'entity_type' => $entityType,
            'sort_order' => Filter::int($request->input('sort_order'), 0),
            'seo_title' => Filter::string($request->input('seo_title'), 255),
            'seo_description' => Filter::string($request->input('seo_description'), 65535),
        ];
    }

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

    /**
     * Список сущностей, к которым может относиться категория. Ядро отдаёт
     * news+page; плагины расширяют через хук `categories.entities`.
     *
     * @return array<int, array{slug:string, label:string}>
     */
    private function entityTypes(): array
    {
        $core = [
            ['slug' => 'news', 'label' => 'Новости'],
            ['slug' => 'page', 'label' => 'Страницы'],
        ];
        $all = Hook::filter('categories.entities', $core);
        if (!is_array($all) || $all === []) return $core;
        $seen = [];
        $out = [];
        foreach ($all as $e) {
            if (!is_array($e) || empty($e['slug'])) continue;
            $slug = (string) $e['slug'];
            if (isset($seen[$slug])) continue;
            $seen[$slug] = true;
            $out[] = ['slug' => $slug, 'label' => (string) ($e['label'] ?? $slug)];
        }
        return $out;
    }

    /** @param array<int, array{slug:string, label:string}> $entities */
    private function isValidEntity(string $slug, array $entities): bool
    {
        foreach ($entities as $e) {
            if (($e['slug'] ?? '') === $slug) return true;
        }
        return false;
    }

    /**
     * Разложить плоский список в порядок обхода дерева, добавляя поле `depth`
     * (0 = корень). Подходит и для сетки в админке, и для вложенного <select>.
     *
     * @param array<int, array<string,mixed>> $rows
     * @return array<int, array<string,mixed>>
     */
    private function flattenTree(array $rows): array
    {
        $byParent = [];
        foreach ($rows as $r) {
            $pid = $r['parent_id'] === null ? 0 : (int) $r['parent_id'];
            $byParent[$pid][] = $r;
        }
        $out = [];
        $visit = function (int $parentId, int $depth) use (&$visit, &$out, $byParent): void {
            foreach ($byParent[$parentId] ?? [] as $row) {
                $row['depth'] = $depth;
                $row['prefix'] = str_repeat('— ', $depth);
                $out[] = $row;
                $visit((int) $row['id'], $depth + 1);
            }
        };
        $visit(0, 0);
        // Сироты (родитель удалён, но parent_id не обнулился) — показываем в конце, чтобы не терялись.
        $seen = array_column($out, 'id');
        foreach ($rows as $r) {
            if (!in_array((int) $r['id'], array_map('intval', $seen), true)) {
                $r['depth'] = 0;
                $r['prefix'] = '';
                $out[] = $r;
            }
        }
        return $out;
    }

    /**
     * Собрать все id-потомков указанной категории (прямые и транзитивные).
     *
     * @param array<int, array<string,mixed>> $rows
     * @return array<int, true>
     */
    private function collectDescendants(array $rows, int $rootId): array
    {
        $byParent = [];
        foreach ($rows as $r) {
            $pid = $r['parent_id'] === null ? 0 : (int) $r['parent_id'];
            $byParent[$pid][] = (int) $r['id'];
        }
        $out = [];
        $stack = [$rootId];
        while ($stack !== []) {
            $cur = (int) array_pop($stack);
            foreach ($byParent[$cur] ?? [] as $childId) {
                if (!isset($out[$childId])) {
                    $out[$childId] = true;
                    $stack[] = $childId;
                }
            }
        }
        return $out;
    }
}
