<?php

declare(strict_types=1);

final class BlogCommentRepository
{
    public const STATUS_PENDING = 'pending';
    public const STATUS_APPROVED = 'approved';
    public const STATUS_REJECTED = 'rejected';
    public const STATUS_SPAM = 'spam';
    public const STATUS_DELETED = 'deleted';

    public const STATUSES = [
        self::STATUS_PENDING,
        self::STATUS_APPROVED,
        self::STATUS_REJECTED,
        self::STATUS_SPAM,
        self::STATUS_DELETED,
    ];

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

    public function createPending(array $data): array
    {
        $now = myronNow();
        $id = $this->generateId();
        $statement = $this->pdo->prepare(
            'INSERT INTO blog_comments (
                id, page_id, page_version_id, author_name, author_email, body, status,
                submitted_at, moderated_at, moderated_by, source_url, ip_hash, user_agent,
                created_at, updated_at
             ) VALUES (
                :id, :page_id, :page_version_id, :author_name, :author_email, :body, :status,
                :submitted_at, NULL, NULL, :source_url, :ip_hash, :user_agent,
                :created_at, :updated_at
             )'
        );
        $statement->execute([
            ':id' => $id,
            ':page_id' => trim((string) ($data['page_id'] ?? '')),
            ':page_version_id' => $this->nullableString($data['page_version_id'] ?? null),
            ':author_name' => trim((string) ($data['author_name'] ?? '')),
            ':author_email' => trim((string) ($data['author_email'] ?? '')),
            ':body' => trim((string) ($data['body'] ?? '')),
            ':status' => self::STATUS_PENDING,
            ':submitted_at' => (string) ($data['submitted_at'] ?? $now),
            ':source_url' => substr(trim((string) ($data['source_url'] ?? '')), 0, 500),
            ':ip_hash' => substr(trim((string) ($data['ip_hash'] ?? '')), 0, 64),
            ':user_agent' => substr(trim((string) ($data['user_agent'] ?? '')), 0, 500),
            ':created_at' => $now,
            ':updated_at' => $now,
        ]);
        return $this->findById($id) ?? [];
    }

    public function findById(string $id): ?array
    {
        $statement = $this->pdo->prepare('SELECT * FROM blog_comments WHERE id = :id LIMIT 1');
        $statement->execute([':id' => trim($id)]);
        $row = $statement->fetch(PDO::FETCH_ASSOC);
        return is_array($row) ? $this->hydrate($row) : null;
    }

    /**
     * @return list<array<string, mixed>>
     */
    public function listForPage(string $pageId, ?string $status = null, int $limit = 50, int $offset = 0): array
    {
        $params = [':page_id' => trim($pageId)];
        $sql = 'SELECT * FROM blog_comments WHERE page_id = :page_id';
        if ($status !== null) {
            $status = $this->normalizeStatus($status);
            $sql .= ' AND status = :status';
            $params[':status'] = $status;
        }
        $sql .= ' ORDER BY submitted_at DESC, id DESC LIMIT :limit OFFSET :offset';
        $statement = $this->pdo->prepare($sql);
        foreach ($params as $key => $value) {
            $statement->bindValue($key, $value);
        }
        $statement->bindValue(':limit', max(1, min(100, $limit)), PDO::PARAM_INT);
        $statement->bindValue(':offset', max(0, $offset), PDO::PARAM_INT);
        $statement->execute();
        return array_map([$this, 'hydrate'], $statement->fetchAll(PDO::FETCH_ASSOC) ?: []);
    }

    /**
     * @return list<array<string, mixed>>
     */
    public function listByStatus(string $status, int $limit = 50, int $offset = 0): array
    {
        $status = $this->normalizeStatus($status);
        $statement = $this->pdo->prepare(
            'SELECT * FROM blog_comments WHERE status = :status ORDER BY submitted_at DESC, id DESC LIMIT :limit OFFSET :offset'
        );
        $statement->bindValue(':status', $status);
        $statement->bindValue(':limit', max(1, min(100, $limit)), PDO::PARAM_INT);
        $statement->bindValue(':offset', max(0, $offset), PDO::PARAM_INT);
        $statement->execute();
        return array_map([$this, 'hydrate'], $statement->fetchAll(PDO::FETCH_ASSOC) ?: []);
    }

    /**
     * @return list<array<string, mixed>>
     */
    public function listForModeration(?string $status = null, int $limit = 50, int $offset = 0): array
    {
        $params = [];
        $sql = 'SELECT * FROM blog_comments';
        if ($status !== null) {
            $status = $this->normalizeStatus($status);
            $sql .= ' WHERE status = :status';
            $params[':status'] = $status;
        }
        $sql .= ' ORDER BY submitted_at DESC, id DESC LIMIT :limit OFFSET :offset';
        $statement = $this->pdo->prepare($sql);
        foreach ($params as $key => $value) {
            $statement->bindValue($key, $value);
        }
        $statement->bindValue(':limit', max(1, min(100, $limit)), PDO::PARAM_INT);
        $statement->bindValue(':offset', max(0, $offset), PDO::PARAM_INT);
        $statement->execute();
        return array_map([$this, 'hydrate'], $statement->fetchAll(PDO::FETCH_ASSOC) ?: []);
    }

    /**
     * @return array<string, int>
     */
    public function countByStatus(): array
    {
        $counts = array_fill_keys(self::STATUSES, 0);
        $statement = $this->pdo->query('SELECT status, COUNT(*) AS comment_count FROM blog_comments GROUP BY status');
        foreach ($statement->fetchAll(PDO::FETCH_ASSOC) ?: [] as $row) {
            $status = trim((string) ($row['status'] ?? ''));
            if (in_array($status, self::STATUSES, true)) {
                $counts[$status] = (int) ($row['comment_count'] ?? 0);
            }
        }
        return $counts;
    }

    public function countApprovedForPage(string $pageId): int
    {
        $statement = $this->pdo->prepare(
            'SELECT COUNT(*) FROM blog_comments WHERE page_id = :page_id AND status = :status'
        );
        $statement->execute([':page_id' => trim($pageId), ':status' => self::STATUS_APPROVED]);
        return (int) $statement->fetchColumn();
    }

    public function transitionStatus(string $id, string $toStatus, ?string $actorId = null): array
    {
        $toStatus = $this->normalizeStatus($toStatus);
        $now = myronNow();
        $moderatedAt = $toStatus === self::STATUS_PENDING ? null : $now;
        $moderatedBy = $toStatus === self::STATUS_PENDING || $actorId === null ? null : trim($actorId);
        $statement = $this->pdo->prepare(
            'UPDATE blog_comments
             SET status = :status,
                 moderated_at = :moderated_at,
                 moderated_by = :moderated_by,
                 updated_at = :updated_at
             WHERE id = :id'
        );
        $statement->execute([
            ':id' => trim($id),
            ':status' => $toStatus,
            ':moderated_at' => $moderatedAt,
            ':moderated_by' => $moderatedBy,
            ':updated_at' => $now,
        ]);
        $record = $this->findById($id);
        if ($record === null) {
            throw new InvalidArgumentException('Comment not found.');
        }
        return $record;
    }

    public function publicPayload(array $comment): array
    {
        return [
            'id' => (string) ($comment['id'] ?? ''),
            'page_id' => (string) ($comment['page_id'] ?? ''),
            'author_name' => (string) ($comment['author_name'] ?? ''),
            'body' => (string) ($comment['body'] ?? ''),
            'status' => (string) ($comment['status'] ?? ''),
            'submitted_at' => (string) ($comment['submitted_at'] ?? ''),
        ];
    }

    public function normalizeStatus(string $status): string
    {
        $status = trim($status);
        if (!in_array($status, self::STATUSES, true)) {
            throw new InvalidArgumentException('Invalid comment status.');
        }
        return $status;
    }

    private function generateId(): string
    {
        do {
            $id = 'bc_' . bin2hex(random_bytes(8));
        } while ($this->findById($id) !== null);
        return $id;
    }

    private function nullableString(mixed $value): ?string
    {
        $value = trim((string) ($value ?? ''));
        return $value === '' ? null : $value;
    }

    private function hydrate(array $row): array
    {
        return [
            'id' => (string) ($row['id'] ?? ''),
            'page_id' => (string) ($row['page_id'] ?? ''),
            'page_version_id' => (string) ($row['page_version_id'] ?? ''),
            'author_name' => (string) ($row['author_name'] ?? ''),
            'author_email' => (string) ($row['author_email'] ?? ''),
            'body' => (string) ($row['body'] ?? ''),
            'status' => (string) ($row['status'] ?? self::STATUS_PENDING),
            'submitted_at' => (string) ($row['submitted_at'] ?? ''),
            'moderated_at' => (string) ($row['moderated_at'] ?? ''),
            'moderated_by' => (string) ($row['moderated_by'] ?? ''),
            'source_url' => (string) ($row['source_url'] ?? ''),
            'ip_hash' => (string) ($row['ip_hash'] ?? ''),
            'user_agent' => (string) ($row['user_agent'] ?? ''),
            'created_at' => (string) ($row['created_at'] ?? ''),
            'updated_at' => (string) ($row['updated_at'] ?? ''),
        ];
    }
}
