<?php
declare(strict_types=1);

namespace Celena\Module\News;

use Celena\Core\Database\Connection;
use Celena\Core\Http\Pagination;
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\HtmlSanitizer;
use Celena\Core\Security\Permission;
use Celena\Core\Support\Markdown;
use Celena\Core\Template\Engine;
use Celena\Module\AdminBase\AdminController;
use Celena\Module\CustomFields\FieldRenderer;
use Celena\Module\CustomFields\FieldRepository;
use Celena\Module\Settings\Options;

/**
 * Управление новостями: CRUD в админке, публичный вывод списков и полной новости.
 * Использует Markdown для body, хуки news.published / news.title.
 */
final class NewsController 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(), 'news.read')) {
            return $this->redirect('/admin', 'Доступ запрещён.', 'error');
        }
        $page = Filter::int($request->query('page', 1), 1);
        $filter = Filter::in($request->query('status'), ['published', 'draft', ''], '');
        $q = $this->conn->builder('celena_news');
        if ($filter !== '') {
            $q->where('status', '=', $filter);
        }
        $total = (clone $q)->count();
        $p = new Pagination($page, 20, $total);
        $rows = $q->orderBy('id', 'desc')->limit($p->perPage)->offset($p->offset)->get();

        $catMap = [];
        foreach ($this->conn->builder('celena_categories')->get() as $c) {
            $catMap[(int) $c['id']] = (string) $c['title'];
        }
        foreach ($rows as &$n) {
            $cid = (int) ($n['category_id'] ?? 0);
            $n['category_title'] = $cid > 0 && isset($catMap[$cid]) ? $catMap[$cid] : '';
        }
        unset($n);

        return $this->view('admin/news/index', [
            'news' => $rows,
            'pagination' => $p->toArray('/admin/news?page=:page' . ($filter !== '' ? '&status=' . $filter : '')),
            'current_filter' => $filter,
        ], 'news', 'Новости');
    }

    public function create(Request $request): Response
    {
        if (!Permission::can($this->auth->user(), 'news.create')) {
            return $this->redirect('/admin', 'Доступ запрещён.', 'error');
        }
        $categories = $this->categoriesTree();
        $repo = new FieldRepository($this->conn);
        $fields = FieldRenderer::prepare($repo->fieldsFor('news'), []);
        return $this->view('admin/news/form', [
            'row' => $this->emptyRow(),
            'categories' => $categories,
            'custom_fields' => $fields,
            'form_action' => '/admin/news/create',
        ], 'news', 'Новая новость');
    }

    public function store(Request $request): Response
    {
        if (!Permission::can($this->auth->user(), 'news.create')) {
            return $this->redirect('/admin', 'Доступ запрещён.', 'error');
        }
        if (!Csrf::validate((string) $request->input('_csrf'))) {
            return $this->redirect('/admin/news/create', 'CSRF.', 'error');
        }
        $data = $this->extract($request);
        if ($data['title'] === '') {
            return $this->redirect('/admin/news/create', 'Заголовок обязателен.', 'error');
        }
        if ($data['slug'] === '') {
            $data['slug'] = Filter::slug($data['title']);
        }
        if ($this->slugExists($data['slug'])) {
            return $this->redirect('/admin/news/create', 'Slug уже существует.', 'error');
        }
        $now = date('Y-m-d H:i:s');
        $data['author_id'] = $this->auth->id();
        $data['created_at'] = $now;
        $data['updated_at'] = $now;
        if ($data['status'] === 'published' && $data['published_at'] === null) {
            $data['published_at'] = $now;
        }
        $id = $this->conn->builder('celena_news')->insert($data);
        $cfValues = $request->input('cf');
        if (is_array($cfValues)) {
            (new FieldRepository($this->conn))->saveValues('news', (int) $id, $cfValues);
        }
        if ($data['status'] === 'published') {
            Hook::action('news.published', array_merge($data, ['id' => (int) $id]));
        }
        return $this->redirect('/admin/news', 'Новость создана.');
    }

    public function edit(Request $request, array $params): Response
    {
        $id = Filter::int($params['id'] ?? 0);
        $row = $this->conn->builder('celena_news')->where('id', '=', $id)->first();
        if ($row === null) {
            return $this->redirect('/admin/news', 'Не найдена.', 'error');
        }
        if (!$this->canManageNews($row, 'update')) {
            return $this->redirect('/admin/news', 'Доступ запрещён.', 'error');
        }
        $categories = $this->categoriesTree();
        $repo = new FieldRepository($this->conn);
        $fields = FieldRenderer::prepare($repo->fieldsFor('news'), $repo->valuesFor('news', $id));
        return $this->view('admin/news/form', [
            'row' => $row,
            'categories' => $categories,
            'custom_fields' => $fields,
            'form_action' => '/admin/news/' . $id . '/update',
        ], 'news', 'Редактировать: ' . $row['title']);
    }

    public function update(Request $request, array $params): Response
    {
        if (!Csrf::validate((string) $request->input('_csrf'))) {
            return $this->redirect('/admin/news', 'CSRF.', 'error');
        }
        $id = Filter::int($params['id'] ?? 0);
        $existing = $this->conn->builder('celena_news')->where('id', '=', $id)->first();
        if ($existing === null) {
            return $this->redirect('/admin/news', 'Не найдена.', 'error');
        }
        if (!$this->canManageNews($existing, 'update')) {
            return $this->redirect('/admin/news', 'Доступ запрещён.', 'error');
        }
        $data = $this->extract($request);
        // author не может менять автора материала.
        $user = $this->auth->user();
        if (!Permission::can($user, 'news.update.any')) {
            unset($data['author_id']);
        }
        if ($data['slug'] === '') {
            $data['slug'] = Filter::slug($data['title']);
        }
        if ($data['slug'] !== $existing['slug'] && $this->slugExists($data['slug'])) {
            return $this->redirect('/admin/news/' . $id . '/edit', 'Slug уже занят.', 'error');
        }
        $wasDraft = $existing['status'] !== 'published';
        $data['updated_at'] = date('Y-m-d H:i:s');
        if ($data['status'] === 'published' && $existing['published_at'] === null && $data['published_at'] === null) {
            $data['published_at'] = $data['updated_at'];
        }
        $this->conn->builder('celena_news')->where('id', '=', $id)->update($data);
        $cfValues = $request->input('cf');
        if (is_array($cfValues)) {
            (new FieldRepository($this->conn))->saveValues('news', $id, $cfValues);
        }
        if ($wasDraft && $data['status'] === 'published') {
            Hook::action('news.published', array_merge($existing, $data, ['id' => $id]));
        }
        return $this->redirect('/admin/news', 'Сохранено.');
    }

    public function delete(Request $request, array $params): Response
    {
        if (!Csrf::validate((string) $request->input('_csrf'))) {
            return $this->redirect('/admin/news', 'CSRF.', 'error');
        }
        $id = Filter::int($params['id'] ?? 0);
        $existing = $this->conn->builder('celena_news')->where('id', '=', $id)->first();
        if ($existing === null) {
            return $this->redirect('/admin/news', 'Не найдена.', 'error');
        }
        if (!$this->canManageNews($existing, 'delete')) {
            return $this->redirect('/admin/news', 'Доступ запрещён.', 'error');
        }
        $this->conn->builder('celena_news')->where('id', '=', $id)->delete();
        return $this->redirect('/admin/news', 'Новость удалена.');
    }

    /**
     * Может ли текущий пользователь редактировать/удалять данную запись.
     * Логика:
     *  - news.{action}.any — управление любыми записями (admin, editor через news.*).
     *  - news.{action}    — управление только своими (author).
     *  - иначе отказ.
     */
    private function canManageNews(array $row, string $action): bool
    {
        $user = $this->auth->user();
        if ($user === null) {
            return false;
        }
        if (Permission::can($user, 'news.' . $action . '.any')) {
            return true;
        }
        if (!Permission::can($user, 'news.' . $action)) {
            return false;
        }
        return (int) ($row['author_id'] ?? 0) === (int) ($user['id'] ?? 0);
    }

    // ====== ФРОНТ ======

    public function publicList(Request $request): Response
    {
        $page = Filter::int($request->query('page', 1), 1);
        $perPage = Filter::int($this->options->get('posts_per_page', 10), 10);
        $q = $this->conn->builder('celena_news')->where('status', '=', 'published');
        $total = (clone $q)->count();
        $p = new Pagination($page, $perPage, $total);
        $rows = $q->orderBy('published_at', 'desc')->limit($p->perPage)->offset($p->offset)->get();
        foreach ($rows as &$r) {
            $r['title'] = (string) Hook::filter('news.title', $r['title'], $r);
            $r['url'] = '/news/' . $r['slug'];
            $r['date'] = (string) ($r['published_at'] ?? '');
        }
        unset($r);

        $this->engine->setTheme((string) $this->options->get('theme', 'default'));
        $html = $this->engine->render('news_list', [
            'site' => ['name' => (string) $this->options->get('site_name', '')],
            'page' => ['title' => 'Новости', 'seo_title' => 'Новости — ' . (string) $this->options->get('site_name', '')],
            'news' => $rows,
            'pagination' => $p->toArray('/news?page=:page'),
        ]);
        return Response::html($html);
    }

    public function publicShow(Request $request, array $params): Response
    {
        $slug = Filter::urlSlug($params['slug'] ?? '');
        $row = $this->conn->builder('celena_news')->where('slug', '=', $slug)->first();
        if ($row === null || $row['status'] !== 'published') {
            return Response::notFound();
        }
        $this->conn->builder('celena_news')->where('id', '=', $row['id'])->update(['views' => (int) $row['views'] + 1]);
        $bodyRaw = (string) ($row['body'] ?? '');
        $bodyHtml = ((string) ($row['body_format'] ?? 'html')) === 'markdown'
            ? Markdown::render($bodyRaw)
            : $bodyRaw;
        $category = null;
        if (!empty($row['category_id'])) {
            $category = $this->conn->builder('celena_categories')->where('id', '=', (int) $row['category_id'])->first();
        }
        $comments = $this->conn->builder('celena_comments')
            ->where('entity_type', '=', 'news')
            ->where('entity_id', '=', (int) $row['id'])
            ->where('status', '=', 'approved')
            ->orderBy('created_at', 'asc')
            ->get();
        $cfRepo = new \Celena\Module\CustomFields\FieldRepository($this->conn);
        $customValues = $cfRepo->valuesFor('news', (int) $row['id']);

        $this->engine->setTheme((string) $this->options->get('theme', 'default'));
        $html = $this->engine->render('news_full', [
            '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['og_image'] ?? $row['cover'] ?? ''),
            ],
            'news' => [
                'id' => (int) $row['id'],
                'title' => (string) Hook::filter('news.title', $row['title'], $row),
                'body' => $bodyHtml,
                'short' => (string) ($row['short'] ?? ''),
                'cover' => (string) ($row['cover'] ?? ''),
                'date' => (string) ($row['published_at'] ?? ''),
                'views' => (int) $row['views'],
                'url' => '/news/' . $row['slug'],
                'slug' => (string) $row['slug'],
                'category' => $category ? ['title' => $category['title'], 'slug' => $category['slug']] : null,
                'fields' => $customValues,
            ],
            'comments' => $comments,
            'comments_open' => (string) $this->options->get('comments_open', '1'),
            'comment_status' => (string) $request->query('c', ''),
            'comment_error'  => (string) $request->query('err', ''),
        ]);
        return Response::html($html);
    }

    /** @return array<int, array<string,mixed>> список категорий новостей, упорядоченных деревом, с полями depth/prefix. */
    private function categoriesTree(): array
    {
        $rows = $this->conn->builder('celena_categories')
            ->where('entity_type', '=', 'news')
            ->orderBy('sort_order', 'asc')->orderBy('title', 'asc')
            ->get();
        $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);
        return $out;
    }

    private function emptyRow(): array
    {
        return [
            'id' => 0, 'slug' => '', 'title' => '', 'short' => '', 'body' => '',
            'body_format' => 'html', 'status' => 'draft', 'category_id' => '',
            'cover' => '', 'seo_title' => '', 'seo_description' => '', 'og_image' => '',
            'views' => 0, 'comments_count' => 0, 'published_at' => null,
        ];
    }

    private function extract(Request $request): array
    {
        $catRaw = $request->input('category_id');
        $cat = ($catRaw === '' || $catRaw === null) ? null : Filter::int($catRaw);
        $bodyFormat = Filter::in($request->input('body_format'), ['markdown', 'html'], 'html');
        $bodyRaw = Filter::string($request->input('body'), 65535 * 4);
        // HTML-тело санитизируем сразу при сохранении: shop visitor никогда не должен
        // получить <script> из новости, даже если author/editor попробует его вставить.
        // Markdown проходит через Markdown::render при рендере страницы.
        $body = $bodyFormat === 'html' ? HtmlSanitizer::clean($bodyRaw) : $bodyRaw;
        return [
            'title' => Filter::string($request->input('title'), 255),
            'slug' => Filter::urlSlug($request->input('slug', '')),
            'short' => Filter::string($request->input('short'), 5000),
            'body' => $body,
            'body_format' => $bodyFormat,
            'status' => Filter::in($request->input('status'), ['published', 'draft'], 'draft'),
            'category_id' => $cat === 0 ? null : $cat,
            'cover' => Filter::string($request->input('cover'), 500),
            'seo_title' => Filter::string($request->input('seo_title'), 255),
            'seo_description' => Filter::string($request->input('seo_description'), 65535),
            'og_image' => Filter::string($request->input('og_image'), 500),
            'published_at' => null,
        ];
    }

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