<?php
declare(strict_types=1);

namespace App\Questions;

use App\Slug\Slugger;
use App\Support\TextNormalizer;
use App\Support\Hooks;
use App\Support\CoreHooks;
use InvalidArgumentException;
use PDO;
use RuntimeException;
use Throwable;

final class QuestionService
{
    private const DEFAULT_LOCALE = 'ar';
    private const MODERATION_ROLES = ['moderator', 'admin'];

    private readonly TextNormalizer $normalizer;
    private readonly Slugger $slugger;

    public function __construct(
        private readonly PDO $pdo,
        ?TextNormalizer $normalizer = null,
        ?Slugger $slugger = null,
    ) {
        $this->normalizer = $normalizer ?? new TextNormalizer();
        $this->slugger = $slugger ?? new Slugger($this->normalizer);
    }

    /**
     * Who may skip the queue.
     *
     * The status used to be a literal in the INSERT, so an administrator's own
     * question waited for an administrator to approve it -- and the only person who
     * could approve it was the person who wrote it.
     */
    public static function decideStatus(array $actor, string $mode, bool $hasPublishedBefore): string
    {
        if (in_array(strtolower((string) ($actor['role'] ?? '')), self::MODERATION_ROLES, true)) {
            return 'published';
        }

        return match ($mode) {
            'none' => 'published',
            'new_members' => $hasPublishedBefore ? 'published' : 'pending',
            default => 'pending',
        };
    }

    public function create(
        int $userId,
        string $title,
        string $body,
        ?int $categoryId = null,
        string $contentLocale = 'ar',
        bool $anonymous = false,
        array $tagIds = [],
        string $status = 'pending'
    ): int {
        $title = trim($title);
        $body = trim($body);
        $contentLocale = $this->normalizeLocale($contentLocale);
        $status = in_array($status, ['published', 'pending', 'draft', 'closed', 'rejected'], true) ? $status : 'pending';

        if ($title === '') throw new \App\Support\TranslatableException('questions.title_required', [], 'Question title is required.');
        if ($body === '') throw new \App\Support\TranslatableException('questions.body_required', [], 'Question body is required.');

        $this->requireActiveUser($userId);
        $this->requireCategory($categoryId, $contentLocale);
        $tagIds = array_values(array_unique(array_filter(array_map('intval', $tagIds), static fn (int $id): bool => $id > 0)));
        if (count($tagIds) > 5) throw new \App\Support\TranslatableException('questions.tags_max', ['count' => 5], 'A question may have at most five tags.');
        $this->requireTags($tagIds, $contentLocale);

        $slugValue = null;
        $publishedAt = null;
        if ($status === 'published') {
            $baseSlug = $this->slugBase($title);
            $slugValue = $this->uniqueSlug($baseSlug, 0);
            $publishedAt = date('Y-m-d H:i:s');
        }

        $this->pdo->beginTransaction();
        try {
            $statement = $this->pdo->prepare(
                'INSERT INTO questions (user_id,category_id,content_locale,slug,title,title_normalized,body,body_normalized,status,is_anonymous,published_at,created_at,updated_at) '
                . 'VALUES (:user_id,:category_id,:content_locale,:slug,:title,:title_normalized,:body,:body_normalized,:status,:is_anonymous,:published_at,NOW(),NOW())'
            );
            $statement->execute([
                'user_id'=>$userId,'category_id'=>$categoryId,'content_locale'=>$contentLocale,'slug'=>$slugValue,'title'=>$title,
                'title_normalized'=>$this->normalizer->normalize($title, $contentLocale),
                'body'=>$body,'body_normalized'=>$this->normalizer->normalize($body, $contentLocale),
                'status'=>$status,'is_anonymous'=>$anonymous?1:0,'published_at'=>$publishedAt,
            ]);
            $questionId=(int)$this->pdo->lastInsertId();
            if ($tagIds !== []) {
                $tag=$this->pdo->prepare('INSERT INTO question_tags (question_id,tag_id,created_at) VALUES (:question_id,:tag_id,NOW())');
                foreach($tagIds as $tagId) $tag->execute(['question_id'=>$questionId,'tag_id'=>$tagId]);
            }
            $this->pdo->commit();
            Hooks::emit(CoreHooks::QUESTION_CREATED, [
                'question_id' => $questionId,
                'user_id' => $userId,
                'content_locale' => $contentLocale,
                'status' => $status,
                'slug' => $slugValue,
            ]);
            if ($status === 'published') {
                Hooks::emit(CoreHooks::QUESTION_PUBLISHED, [
                    'question_id' => $questionId,
                    'user_id' => $userId,
                    'content_locale' => $contentLocale,
                    'slug' => $slugValue,
                ]);
            }
            return $questionId;
        } catch (Throwable $throwable) {
            if ($this->pdo->inTransaction()) $this->pdo->rollBack();
            throw $throwable;
        }
    }

    /** @param list<int> $tagIds */
    private function requireTags(array $tagIds, string $contentLocale): void
    {
        if ($tagIds === []) return;
        $placeholders=implode(',',array_fill(0,count($tagIds),'?'));
        $statement=$this->pdo->prepare("SELECT id, content_locale FROM tags WHERE id IN ({$placeholders})");
        $statement->execute($tagIds);
        $rows=$statement->fetchAll(PDO::FETCH_ASSOC);
        if (count($rows) !== count($tagIds)) throw new InvalidArgumentException('One or more selected tags do not exist.');
        foreach ($rows as $row) {
            if ($this->normalizeLocale((string)($row['content_locale'] ?? '')) !== $contentLocale) {
                throw new InvalidArgumentException('One or more selected tags use a different content_locale than the question.');
            }
        }
    }

    public function publish(int $actorId, int $questionId, ?string $slug = null, ?string $metaDescription = null): bool
    {
        $this->requireModerator($actorId);

        $question = $this->requireQuestion($questionId);

        if (($question['status'] ?? null) === 'published') {
            return true;
        }

        $this->requireQuestionStatus($question, ['pending'], 'Only pending questions may be published.');

        $metaDescription = $metaDescription !== null ? trim($metaDescription) : ($question['meta_description'] ?? null);
        if ($metaDescription === '') {
            $metaDescription = null;
        }

        $slugValue = $slug !== null ? $this->normalizeSlug($slug) : trim((string) ($question['slug'] ?? ''));
        if ($slugValue === '') {
            $slugValue = $this->slugBase((string) ($question['title'] ?? ''));
        }

        $slugValue = $this->uniqueSlug($slugValue, $questionId);

        $statement = $this->pdo->prepare(
            'UPDATE questions
             SET status = :status,
                 slug = :slug,
                 meta_description = :meta_description,
                 published_at = COALESCE(published_at, NOW()),
                 updated_at = NOW()
             WHERE id = :id'
        );
        $statement->execute([
            'status' => 'published',
            'slug' => $slugValue,
            'meta_description' => $metaDescription,
            'id' => $questionId,
        ]);

        Hooks::emit(CoreHooks::QUESTION_PUBLISHED, [
            'question_id' => $questionId,
            'actor_id' => $actorId,
            'user_id' => (int) ($question['user_id'] ?? 0),
            'content_locale' => (string) ($question['content_locale'] ?? self::DEFAULT_LOCALE),
            'slug' => $slugValue,
        ]);

        return true;
    }

    public function reject(int $actorId, int $questionId, string $reason): bool
    {
        $this->requireModerator($actorId);

        $reason = trim($reason);
        if ($reason === '') {
            throw new InvalidArgumentException('Rejection reason is required.');
        }

        $question = $this->requireQuestion($questionId);
        $this->requireQuestionStatus($question, ['pending'], 'Only pending questions may be rejected.');

        $statement = $this->pdo->prepare(
            'UPDATE questions
             SET status = :status,
                 rejected_reason = :rejected_reason,
                 updated_at = NOW()
             WHERE id = :id'
        );
        $statement->execute([
            'status' => 'rejected',
            'rejected_reason' => $reason,
            'id' => $questionId,
        ]);

        return true;
    }

    public function close(int $actorId, int $questionId): bool
    {
        $this->requireModerator($actorId);
        $question = $this->requireQuestion($questionId);
        $this->requireQuestionStatus($question, ['published'], 'Only published questions may be closed.');

        $statement = $this->pdo->prepare(
            'UPDATE questions
             SET status = :status,
                 updated_at = NOW()
             WHERE id = :id'
        );
        $statement->execute([
            'status' => 'closed',
            'id' => $questionId,
        ]);

        return true;
    }

    public function markDuplicate(int $actorId, int $questionId, int $originalId): bool
    {
        $this->requireModerator($actorId);

        if ($questionId === $originalId) {
            throw new RuntimeException('A question cannot be marked as a duplicate of itself.');
        }

        $question = $this->requireQuestion($questionId);
        $this->requireQuestionStatus($question, ['published'], 'Only published questions may be marked duplicate.');
        $original = $this->requireQuestion($originalId);

        if (($original['status'] ?? null) !== 'published') {
            throw new RuntimeException('Duplicate originals must be published questions.');
        }

        $this->assertNoDuplicateCycle($questionId, $originalId);

        $statement = $this->pdo->prepare(
            'UPDATE questions
             SET status = :status,
                 duplicate_of_id = :duplicate_of_id,
                 updated_at = NOW()
             WHERE id = :id'
        );
        $statement->execute([
            'status' => 'duplicate',
            'duplicate_of_id' => $originalId,
            'id' => $questionId,
        ]);

        return true;
    }

    public function findBySlug(string $slug): ?array
    {
        $slug = trim($slug);

        if ($slug === '') {
            return null;
        }

        $statement = $this->pdo->prepare(
            'SELECT *
             FROM questions
             WHERE slug = :slug
               AND status = :status
             LIMIT 1'
        );
        $statement->execute([
            'slug' => $slug,
            'status' => 'published',
        ]);
        $question = $statement->fetch(PDO::FETCH_ASSOC);

        return is_array($question) ? $question : null;
    }

    public function duplicateRedirectSlug(string $slug): ?string
    {
        $slug = trim($slug);

        if ($slug === '') {
            return null;
        }

        $statement = $this->pdo->prepare(
            'SELECT original.slug
             FROM questions duplicate
             INNER JOIN questions original ON original.id = duplicate.duplicate_of_id
             WHERE duplicate.slug = :slug
               AND duplicate.status = :duplicate_status
               AND original.status = :published_status
               AND original.slug IS NOT NULL
             LIMIT 1'
        );
        $statement->execute([
            'slug' => $slug,
            'duplicate_status' => 'duplicate',
            'published_status' => 'published',
        ]);
        $redirectSlug = $statement->fetchColumn();

        return is_string($redirectSlug) && trim($redirectSlug) !== '' ? trim($redirectSlug) : null;
    }

    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]);
        $user = $statement->fetch(PDO::FETCH_ASSOC);

        if (!is_array($user) || ($user['status'] ?? null) !== 'active') {
            throw new RuntimeException('An active user is required.');
        }

        return $user;
    }

    private function requireModerator(int $actorId): void
    {
        $actor = $this->requireActiveUser($actorId);

        if (!quaeriva_user_has_role($actor, ...self::MODERATION_ROLES)) {
            throw new RuntimeException('Moderator or admin privileges are required.');
        }
    }

    private function requireCategory(?int $categoryId, string $contentLocale): void
    {
        if ($categoryId === null) {
            return;
        }

        $statement = $this->pdo->prepare(
            'SELECT id, content_locale
             FROM categories
             WHERE id = :id
             LIMIT 1'
        );
        $statement->execute(['id' => $categoryId]);
        $category=$statement->fetch(PDO::FETCH_ASSOC);

        if (!is_array($category)) {
            throw new RuntimeException('The selected category does not exist.');
        }
        if ($this->normalizeLocale((string)($category['content_locale'] ?? '')) !== $contentLocale) {
            throw new InvalidArgumentException('The selected category uses a different content_locale than the question.');
        }
    }

    private function requireQuestion(int $questionId): array
    {
        $statement = $this->pdo->prepare(
            'SELECT *
             FROM questions
             WHERE id = :id
             LIMIT 1'
        );
        $statement->execute(['id' => $questionId]);
        $question = $statement->fetch(PDO::FETCH_ASSOC);

        if (!is_array($question)) {
            throw new RuntimeException('Question not found.');
        }

        return $question;
    }

    private function requireQuestionStatus(array $question, array $allowedStatuses, string $message): void
    {
        $status = (string) ($question['status'] ?? '');

        if (!in_array($status, $allowedStatuses, true)) {
            throw new RuntimeException($message);
        }
    }

    private function assertNoDuplicateCycle(int $questionId, int $originalId): void
    {
        $visited = [];
        $currentId = $originalId;

        while ($currentId !== null) {
            if ($currentId === $questionId) {
                throw new RuntimeException('Duplicate mappings must not create redirect cycles.');
            }

            if (isset($visited[$currentId])) {
                throw new RuntimeException('Duplicate mappings must not follow an existing cycle.');
            }

            $visited[$currentId] = true;

            $statement = $this->pdo->prepare(
                'SELECT duplicate_of_id
                 FROM questions
                 WHERE id = :id
                 LIMIT 1'
            );
            $statement->execute(['id' => $currentId]);
            $next = $statement->fetchColumn();

            if ($next === false || $next === null) {
                $currentId = null;
                continue;
            }

            $currentId = (int) $next;
        }
    }

    private function normalizeLocale(string $contentLocale): string
    {
        $contentLocale = strtolower(str_replace('_', '-', trim($contentLocale)));
        if ($contentLocale === '' || preg_match('/^[a-z]{2,3}(?:-[a-z0-9]{2,8})*$/', $contentLocale) !== 1) {
            return self::DEFAULT_LOCALE;
        }

        try {
            $statement = $this->pdo->prepare('SELECT 1 FROM languages WHERE code = :code AND is_enabled = 1 LIMIT 1');
            $statement->execute(['code' => $contentLocale]);
            if ($statement->fetchColumn()) {
                return $contentLocale;
            }
        } catch (\Throwable) {
            if (in_array($contentLocale, ['ar', 'en'], true)) {
                return $contentLocale;
            }
        }

        return self::DEFAULT_LOCALE;
    }

    private function normalizeSlug(string $slug): string
    {
        $slug = trim($slug);

        if ($slug === '') {
            throw new InvalidArgumentException('Slug must not be empty.');
        }

        return $this->slugBase($slug);
    }

    private function slugBase(string $value): string
    {
        return $this->slugger->slug($value);
    }

    private function uniqueSlug(string $base, int $questionId): string
    {
        $candidate = $base;
        $suffix = 2;

        while ($this->slugExists($candidate, $questionId)) {
            $candidate = $base . '-' . $suffix;
            $suffix++;
        }

        return $candidate;
    }

    private function slugExists(string $slug, int $questionId): bool
    {
        $statement = $this->pdo->prepare(
            'SELECT id
             FROM questions
             WHERE slug = :slug
               AND id <> :id
             LIMIT 1'
        );
        $statement->execute([
            'slug' => $slug,
            'id' => $questionId,
        ]);

        return $statement->fetchColumn() !== false;
    }
}
