<?php
declare(strict_types=1);

namespace App\Topics;

use App\Community\PostingPolicyService;
use App\Security\HtmlSanitizer;
use App\Slug\Slugger;
use App\Support\Hooks;
use App\Support\CoreHooks;
use App\Support\TranslatableException;
use PDO;
use Throwable;

final class TopicService
{
    private const KINDS = ['discussion', 'announcement'];
    private const STATUSES = ['published', 'pending', 'hidden', 'draft'];
    private const MODERATION_ROLES = ['moderator', 'admin'];

    public function __construct(
        private readonly PDO $pdo,
        private readonly ?PostingPolicyService $policy = null,
        private readonly ?Slugger $slugger = null,
        private readonly ?HtmlSanitizer $sanitizer = null,
    ) {
    }

    public function create(
        int $userId,
        string $title,
        string $body,
        ?int $categoryId,
        string $locale,
        string $kind = 'discussion',
        string $status = 'published'
    ): int {
        $actor = $this->requireActiveUser($userId);
        $kind = $this->validateKind($kind);
        $status = $this->validateStatus($status);
        $this->assertAnnouncementPermission($actor, $kind);
        [$title, $body, $locale] = $this->validateContent($title, $body, $categoryId, $locale);
        $slug = $this->uniqueSlug($title, 0);

        $ownsTransaction = !$this->pdo->inTransaction();
        if ($ownsTransaction) $this->pdo->beginTransaction();
        try {
            $statement = $this->pdo->prepare(
                'INSERT INTO topics (user_id,category_id,content_locale,title,slug,body,kind,status,last_activity_at,created_at,updated_at)
                 VALUES (:user_id,:category_id,:content_locale,:title,:slug,:body,:kind,:status,NOW(),NOW(),NOW())'
            );
            $statement->execute([
                'user_id' => $userId,
                'category_id' => $categoryId,
                'content_locale' => $locale,
                'title' => $title,
                'slug' => $slug,
                'body' => $body,
                'kind' => $kind,
                'status' => $status,
            ]);
            $topicId = (int) $this->pdo->lastInsertId();
            if ($ownsTransaction) $this->pdo->commit();
            $payload = ['topic_id' => $topicId, 'user_id' => $userId, 'kind' => $kind, 'status' => $status, 'slug' => $slug];
            Hooks::emit(CoreHooks::TOPIC_CREATED, $payload);
            if ($status === 'published') Hooks::emit(CoreHooks::TOPIC_PUBLISHED, $payload);
            return $topicId;
        } catch (Throwable $throwable) {
            if ($ownsTransaction && $this->pdo->inTransaction()) $this->pdo->rollBack();
            throw $throwable;
        }
    }

    public function update(
        int $actorId,
        int $topicId,
        string $title,
        string $body,
        ?int $categoryId,
        string $locale,
        string $kind = 'discussion',
        ?string $status = null
    ): bool {
        $actor = $this->requireActiveUser($actorId);
        $topic = $this->requireTopic($topicId);
        if ((int) ($topic['user_id'] ?? 0) !== $actorId && !$this->isModerator($actor)) {
            throw new TranslatableException('topics.edit_forbidden');
        }
        $kind = $this->validateKind($kind);
        $this->assertAnnouncementPermission($actor, $kind);
        $statusValue = $status === null ? (string) ($topic['status'] ?? 'published') : $this->validateStatus($status);
        [$title, $body, $locale] = $this->validateContent($title, $body, $categoryId, $locale);
        $slug = $this->uniqueSlug($title, $topicId);

        $ownsTransaction = !$this->pdo->inTransaction();
        if ($ownsTransaction) $this->pdo->beginTransaction();
        try {
            $statement = $this->pdo->prepare(
                'UPDATE topics SET category_id=:category_id,content_locale=:content_locale,title=:title,slug=:slug,body=:body,kind=:kind,status=:status,last_activity_at=NOW(),updated_at=NOW() WHERE id=:id'
            );
            $statement->execute([
                'category_id' => $categoryId,
                'content_locale' => $locale,
                'title' => $title,
                'slug' => $slug,
                'body' => $body,
                'kind' => $kind,
                'status' => $statusValue,
                'id' => $topicId,
            ]);
            if ($ownsTransaction) $this->pdo->commit();
        } catch (Throwable $throwable) {
            if ($ownsTransaction && $this->pdo->inTransaction()) $this->pdo->rollBack();
            throw $throwable;
        }
        Hooks::emit(CoreHooks::TOPIC_UPDATED, ['topic_id' => $topicId, 'actor_id' => $actorId, 'slug' => $slug, 'status' => $statusValue]);
        if (($topic['status'] ?? '') !== 'published' && $statusValue === 'published') {
            Hooks::emit(CoreHooks::TOPIC_PUBLISHED, ['topic_id' => $topicId, 'actor_id' => $actorId, 'slug' => $slug]);
        }
        return true;
    }

    public function setPinned(int $actorId, int $topicId, bool $pinned): bool
    {
        $this->requireModerator($actorId);
        $this->requireTopic($topicId);
        $ownsTransaction = !$this->pdo->inTransaction();
        if ($ownsTransaction) $this->pdo->beginTransaction();
        try {
            $statement = $this->pdo->prepare('UPDATE topics SET is_pinned=:is_pinned,updated_at=NOW() WHERE id=:id');
            $statement->execute(['is_pinned' => $pinned ? 1 : 0, 'id' => $topicId]);
            if ($ownsTransaction) $this->pdo->commit();
        } catch (Throwable $throwable) {
            if ($ownsTransaction && $this->pdo->inTransaction()) $this->pdo->rollBack();
            throw $throwable;
        }
        Hooks::emit(CoreHooks::TOPIC_UPDATED, ['topic_id' => $topicId, 'actor_id' => $actorId, 'is_pinned' => $pinned]);
        return true;
    }

    public function setLocked(int $actorId, int $topicId, bool $locked): bool
    {
        $this->requireModerator($actorId);
        $this->requireTopic($topicId);
        $ownsTransaction = !$this->pdo->inTransaction();
        if ($ownsTransaction) $this->pdo->beginTransaction();
        try {
            $statement = $this->pdo->prepare('UPDATE topics SET is_locked=:is_locked,updated_at=NOW() WHERE id=:id');
            $statement->execute(['is_locked' => $locked ? 1 : 0, 'id' => $topicId]);
            if ($ownsTransaction) $this->pdo->commit();
        } catch (Throwable $throwable) {
            if ($ownsTransaction && $this->pdo->inTransaction()) $this->pdo->rollBack();
            throw $throwable;
        }
        Hooks::emit($locked ? CoreHooks::TOPIC_LOCKED : CoreHooks::TOPIC_UNLOCKED, ['topic_id' => $topicId, 'actor_id' => $actorId]);
        return true;
    }

    public function setStatus(int $actorId, int $topicId, string $status): bool
    {
        $this->requireModerator($actorId);
        $topic = $this->requireTopic($topicId);
        $status = $this->validateStatus($status);
        $ownsTransaction = !$this->pdo->inTransaction();
        if ($ownsTransaction) $this->pdo->beginTransaction();
        try {
            $statement = $this->pdo->prepare('UPDATE topics SET status=:status,updated_at=NOW(),last_activity_at=NOW() WHERE id=:id');
            $statement->execute(['status' => $status, 'id' => $topicId]);
            if ($ownsTransaction) $this->pdo->commit();
        } catch (Throwable $throwable) {
            if ($ownsTransaction && $this->pdo->inTransaction()) $this->pdo->rollBack();
            throw $throwable;
        }
        $payload = ['topic_id' => $topicId, 'actor_id' => $actorId, 'status' => $status];
        if (($topic['status'] ?? '') !== 'published' && $status === 'published') Hooks::emit(CoreHooks::TOPIC_PUBLISHED, $payload);
        else Hooks::emit(CoreHooks::TOPIC_UPDATED, $payload);
        return true;
    }

    public function delete(int $actorId, int $topicId): bool
    {
        $this->requireModerator($actorId);
        $this->requireTopic($topicId);
        $ownsTransaction = !$this->pdo->inTransaction();
        if ($ownsTransaction) $this->pdo->beginTransaction();
        try {
            $reports = $this->pdo->prepare("DELETE FROM reports WHERE reportable_type='topic' AND reportable_id=:id");
            $reports->execute(['id' => $topicId]);
            $comments = $this->pdo->prepare("DELETE FROM comments WHERE commentable_type='topic' AND commentable_id=:id");
            $comments->execute(['id' => $topicId]);
            $statement = $this->pdo->prepare('DELETE FROM topics WHERE id=:id');
            $statement->execute(['id' => $topicId]);
            if ($ownsTransaction) $this->pdo->commit();
            Hooks::emit(CoreHooks::TOPIC_UPDATED, ['topic_id' => $topicId, 'actor_id' => $actorId, 'deleted' => true]);
            return true;
        } catch (Throwable $throwable) {
            if ($ownsTransaction && $this->pdo->inTransaction()) $this->pdo->rollBack();
            throw $throwable;
        }
    }

    /** @return array<string,mixed> */
    public function find(int $topicId): array
    {
        return $this->requireTopic($topicId);
    }

    private function validateKind(string $kind): string
    {
        $kind = strtolower(trim($kind));
        if (!in_array($kind, self::KINDS, true)) throw new TranslatableException('topics.invalid_kind');
        return $kind;
    }

    private function validateStatus(string $status): string
    {
        $status = strtolower(trim($status));
        if (!in_array($status, self::STATUSES, true)) throw new TranslatableException('topics.invalid_status');
        return $status;
    }

    /** @return array{0:string,1:string,2:string} */
    private function validateContent(string $title, string $body, ?int $categoryId, string $locale): array
    {
        $title = trim($title);
        $body = trim($body);
        if ($title === '') throw new TranslatableException('topics.title_required');
        if ($body === '') throw new TranslatableException('topics.body_required');
        try {
            $body = $this->sanitizer()->sanitize($body);
        } catch (Throwable $throwable) {
            throw new TranslatableException('topics.body_invalid', [], '', $throwable);
        }
        $locale = $this->normalizeLocale($locale);
        $this->requireCategory($categoryId, $locale);
        $this->policy()->validateQuestion($title, $body, 0);
        return [$title, $body, $locale];
    }

    private function normalizeLocale(string $locale): string
    {
        $locale = strtolower(str_replace('_', '-', trim($locale)));
        if ($locale === '' || preg_match('/^[a-z]{2,3}(?:-[a-z0-9]{2,8})*$/', $locale) !== 1) {
            throw new TranslatableException('topics.locale_invalid');
        }
        $statement = $this->pdo->prepare('SELECT 1 FROM languages WHERE code=:code AND is_enabled=1 LIMIT 1');
        $statement->execute(['code' => $locale]);
        if ($statement->fetchColumn() === false) throw new TranslatableException('topics.locale_not_enabled');
        return $locale;
    }

    private function requireCategory(?int $categoryId, string $locale): void
    {
        if ($categoryId === null || $categoryId <= 0) return;
        $statement = $this->pdo->prepare('SELECT content_locale FROM categories WHERE id=:id LIMIT 1');
        $statement->execute(['id' => $categoryId]);
        $categoryLocale = $statement->fetchColumn();
        if ($categoryLocale === false) throw new TranslatableException('topics.category_not_found');
        if (strtolower(str_replace('_', '-', (string) $categoryLocale)) !== $locale) throw new TranslatableException('topics.category_locale');
    }

    private function assertAnnouncementPermission(array $actor, string $kind): void
    {
        if ($kind === 'announcement' && !$this->isModerator($actor)) throw new TranslatableException('topics.announcement_forbidden');
    }

    private function isModerator(array $actor): bool
    {
        return in_array(strtolower((string) ($actor['role'] ?? '')), self::MODERATION_ROLES, true);
    }

    /** @return array<string,mixed> */
    private function requireActiveUser(int $userId): array
    {
        $statement = $this->pdo->prepare('SELECT id,role,status FROM users WHERE id=:id LIMIT 1');
        $statement->execute(['id' => $userId]);
        $actor = $statement->fetch(PDO::FETCH_ASSOC);
        if (!is_array($actor) || ($actor['status'] ?? '') !== 'active') throw new TranslatableException('topics.active_user_required');
        return $actor;
    }

    private function requireModerator(int $actorId): array
    {
        $actor = $this->requireActiveUser($actorId);
        if (!$this->isModerator($actor)) throw new TranslatableException('topics.moderator_required');
        return $actor;
    }

    /** @return array<string,mixed> */
    private function requireTopic(int $topicId): array
    {
        if ($topicId <= 0) throw new TranslatableException('topics.not_found');
        $statement = $this->pdo->prepare('SELECT * FROM topics WHERE id=:id LIMIT 1');
        $statement->execute(['id' => $topicId]);
        $topic = $statement->fetch(PDO::FETCH_ASSOC);
        if (!is_array($topic)) throw new TranslatableException('topics.not_found');
        return $topic;
    }

    private function uniqueSlug(string $title, int $topicId): string
    {
        return $this->slugger()->unique($title, function (string $candidate) use ($topicId): bool {
            $statement = $this->pdo->prepare('SELECT id FROM topics WHERE slug=:slug AND id<>:id LIMIT 1');
            $statement->execute(['slug' => $candidate, 'id' => $topicId]);
            return $statement->fetchColumn() !== false;
        }, 191);
    }

    private function policy(): PostingPolicyService
    {
        return $this->policy ?? new PostingPolicyService();
    }

    private function slugger(): Slugger
    {
        return $this->slugger ?? new Slugger();
    }

    private function sanitizer(): HtmlSanitizer
    {
        return $this->sanitizer ?? new HtmlSanitizer();
    }
}
