<?php
declare(strict_types=1);

namespace App\Community;

use App\Support\Hooks;
use App\Support\CoreHooks;
use InvalidArgumentException;
use PDO;
use RuntimeException;
use Throwable;

final class CommentService
{
    public function __construct(private readonly PDO $pdo) {}

    public function create(int $userId, string $type, int $targetId, string $body, int $duplicateWindowMinutes = 10, string $status = 'published'): int
    {
        $type = strtolower(trim($type));
        $body = trim($body);
        $status = strtolower(trim($status));
        if (!in_array($status, ['published','pending'], true)) throw new InvalidArgumentException('Unsupported comment status.');
        if (!in_array($type, ['question', 'answer', 'topic'], true)) throw new InvalidArgumentException('Unsupported comment target.');
        $length = function_exists('mb_strlen') ? mb_strlen($body, 'UTF-8') : strlen($body);
        if ($length < 1 || $length > 20000) throw new InvalidArgumentException('Comment is outside the absolute storage limit.');
        $this->requireActiveUser($userId);
        $questionId = null;
        if ($type === 'topic') {
            $this->assertTopicCommentable($targetId);
        } else {
            $questionId = $this->targetQuestionId($type, $targetId);
        }
        $this->assertNotRecentDuplicate($userId, $type, $targetId, $body, $duplicateWindowMinutes);

        $ownsTransaction = !$this->pdo->inTransaction();
        if ($ownsTransaction) $this->pdo->beginTransaction();
        try {
            $statement = $this->pdo->prepare(
                'INSERT INTO comments (commentable_type, commentable_id, user_id, body, status, created_at, updated_at) VALUES (:type, :target_id, :user_id, :body, :status, NOW(), NOW())'
            );
            $statement->execute([
                'type' => $type,
                'target_id' => $targetId,
                'user_id' => $userId,
                'body' => $body,
                'status' => $status,
            ]);
            $commentId = (int) $this->pdo->lastInsertId();
            if ($status === 'published') {
                if ($type === 'topic') {
                    $count = $this->pdo->prepare('UPDATE topics SET comments_count = comments_count + 1, last_activity_at = NOW(), updated_at = NOW() WHERE id = :id');
                } else {
                    $count = $this->pdo->prepare('UPDATE questions SET comments_count = comments_count + 1, updated_at = NOW() WHERE id = :id');
                }
                $count->execute(['id' => $questionId ?? $targetId]);
            }
            if ($ownsTransaction) $this->pdo->commit();
            $payload = [
                'comment_id' => $commentId,
                'user_id' => $userId,
                'target_type' => $type,
                'target_id' => $targetId,
                'status' => $status,
            ];
            if ($questionId !== null) $payload['question_id'] = $questionId;
            Hooks::emit(CoreHooks::COMMENT_CREATED, $payload);
            if ($status === 'published' && $type === 'topic') Hooks::emit(CoreHooks::TOPIC_COMMENT_CREATED, $payload);
            return $commentId;
        } catch (Throwable $throwable) {
            if ($ownsTransaction && $this->pdo->inTransaction()) $this->pdo->rollBack();
            throw $throwable;
        }
    }

    /** @param list<int> $answerIds @return array{question:list<array<string,mixed>>,answers:array<int,list<array<string,mixed>>>} */
    public function forQuestion(int $questionId, array $answerIds = []): array
    {
        $question = $this->pdo->prepare(
            "SELECT c.id,c.commentable_id,c.user_id,c.body,c.created_at,c.updated_at,u.name AS author_name
             FROM comments c LEFT JOIN users u ON u.id=c.user_id
             WHERE c.commentable_type='question' AND c.commentable_id=:question_id AND c.status='published'
             ORDER BY c.created_at ASC,c.id ASC
             LIMIT 50"
        );
        $question->execute(['question_id' => $questionId]);

        $answerIds = array_slice(array_values(array_unique(array_filter(
            array_map('intval', $answerIds),
            static fn (int $id): bool => $id > 0
        ))), 0, 50);
        $answerGroups = [];
        $answers = $this->pdo->prepare(
            "SELECT c.id,c.commentable_id,c.user_id,c.body,c.created_at,c.updated_at,u.name AS author_name
             FROM comments c
             INNER JOIN answers a ON a.id=c.commentable_id
             LEFT JOIN users u ON u.id=c.user_id
             WHERE c.commentable_type='answer'
               AND a.question_id=:question_id
               AND a.id=:answer_id
               AND c.status='published'
             ORDER BY c.created_at ASC,c.id ASC
             LIMIT 50"
        );
        foreach ($answerIds as $answerId) {
            $answers->execute(['question_id' => $questionId, 'answer_id' => $answerId]);
            $rows = $answers->fetchAll(PDO::FETCH_ASSOC);
            if ($rows !== []) $answerGroups[$answerId] = $rows;
        }
        return ['question' => $question->fetchAll(PDO::FETCH_ASSOC), 'answers' => $answerGroups];
    }

    /** @return array{comments:list<array<string,mixed>>,total:int,page:int,per_page:int,total_pages:int} */
    public function forTopic(int $topicId, int $page, int $perPage = 50): array
    {
        $page = max(1, $page);
        $perPage = max(1, min(100, $perPage));
        $totalStatement = $this->pdo->prepare(
            "SELECT COUNT(*) FROM comments WHERE commentable_type='topic' AND commentable_id=:topic_id AND status='published'"
        );
        $totalStatement->execute(['topic_id' => $topicId]);
        $total = (int) $totalStatement->fetchColumn();
        $totalPages = max(1, (int) ceil($total / $perPage));
        $page = min($page, $totalPages);
        $offset = ($page - 1) * $perPage;
        $statement = $this->pdo->prepare(
            "SELECT c.id,c.commentable_id,c.user_id,c.body,c.created_at,c.updated_at,u.name AS author_name
             FROM comments c LEFT JOIN users u ON u.id=c.user_id
             WHERE c.commentable_type='topic' AND c.commentable_id=:topic_id AND c.status='published'
             ORDER BY c.created_at ASC,c.id ASC
             LIMIT {$perPage} OFFSET {$offset}"
        );
        $statement->execute(['topic_id' => $topicId]);
        return [
            'comments' => $statement->fetchAll(PDO::FETCH_ASSOC),
            'total' => $total,
            'page' => $page,
            'per_page' => $perPage,
            'total_pages' => $totalPages,
        ];
    }

    private function assertNotRecentDuplicate(int $userId, string $type, int $targetId, string $body, int $duplicateWindowMinutes): void
    {
        $duplicateWindowMinutes = max(0, min(1440, $duplicateWindowMinutes));
        if ($duplicateWindowMinutes === 0) return;
        $statement = $this->pdo->prepare(
            "SELECT COUNT(*) FROM comments
             WHERE user_id=:user_id
               AND commentable_type=:type
               AND commentable_id=:target_id
               AND body=:body
               AND status='published'
               AND created_at >= DATE_SUB(NOW(), INTERVAL {$duplicateWindowMinutes} MINUTE)"
        );
        $statement->execute(['user_id'=>$userId,'type'=>$type,'target_id'=>$targetId,'body'=>$body]);
        if ((int) $statement->fetchColumn() > 0) {
            throw new \App\Support\TranslatableException('questions.duplicate_post', [], 'This comment was already posted recently.');
        }
    }

    private function targetQuestionId(string $type, int $targetId): int
    {
        if ($targetId <= 0) throw new InvalidArgumentException('Invalid comment target.');
        if ($type === 'question') {
            $statement = $this->pdo->prepare("SELECT id FROM questions WHERE id=:id AND status='published' LIMIT 1");
            $statement->execute(['id' => $targetId]);
            $id = $statement->fetchColumn();
            if ($id === false) throw new RuntimeException('Question not found.');
            return (int) $id;
        }
        $statement = $this->pdo->prepare(
            "SELECT q.id FROM answers a INNER JOIN questions q ON q.id=a.question_id WHERE a.id=:id AND a.status='published' AND q.status='published' LIMIT 1"
        );
        $statement->execute(['id' => $targetId]);
        $id = $statement->fetchColumn();
        if ($id === false) throw new RuntimeException('Answer not found.');
        return (int) $id;
    }

    private function assertTopicCommentable(int $topicId): void
    {
        if ($topicId <= 0) throw new \App\Support\TranslatableException('topics.not_found');
        $statement = $this->pdo->prepare(
            "SELECT id,is_locked FROM topics WHERE id=:id AND status='published' LIMIT 1"
        );
        $statement->execute(['id' => $topicId]);
        $topic = $statement->fetch(PDO::FETCH_ASSOC);
        if (!is_array($topic)) throw new \App\Support\TranslatableException('topics.not_found');
        if ((int) ($topic['is_locked'] ?? 0) === 1) {
            throw new \App\Support\TranslatableException('topics.locked');
        }
    }

    private function requireActiveUser(int $userId): void
    {
        $statement = $this->pdo->prepare("SELECT 1 FROM users WHERE id=:id AND status='active' LIMIT 1");
        $statement->execute(['id' => $userId]);
        if ($statement->fetchColumn() === false) throw new RuntimeException('Active user required.');
    }
}
