<?php

namespace App\Blog\Repository;

use App\Blog\Entity\Blog;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;

class BlogRepository extends ServiceEntityRepository
{
    public function __construct(ManagerRegistry $registry) { parent::__construct($registry, Blog::class); }

    public function findByUuid(string $uuid): ?Blog { return $this->findOneBy(['uuid' => $uuid]); }
    public function findBySlug(string $slug): ?Blog { return $this->findOneBy(['slug' => $slug]); }
    public function findByTopicSlug(string $topicSlug): ?Blog { return $this->findOneBy(['topicSlug' => $topicSlug]); }

    /**
     * The admin review queue: posts awaiting a doctor's decision, newest first.
     * @return Blog[]
     */
    public function findByReviewStatus(string $reviewStatus, int $page = 1, int $limit = 20): array
    {
        return $this->createQueryBuilder('b')
            ->leftJoin('b.city', 'c')->addSelect('c')
            ->leftJoin('b.reviewer', 'r')->addSelect('r')
            ->where('b.reviewStatus = :rs')
            ->setParameter('rs', $reviewStatus)
            ->orderBy('b.createdAt', 'DESC')
            ->setFirstResult(($page - 1) * $limit)
            ->setMaxResults($limit)
            ->getQuery()->getResult();
    }

    public function countByReviewStatus(string $reviewStatus): int
    {
        return (int) $this->createQueryBuilder('b')
            ->select('COUNT(b.id)')
            ->where('b.reviewStatus = :rs')
            ->setParameter('rs', $reviewStatus)
            ->getQuery()->getSingleScalarResult();
    }

    /**
     * Admin list — ALL statuses (draft/published/archived), newest first, with
     * optional status + title search. The public list only returns published, so
     * the admin panel must use this to see drafts.
     * @return Blog[]
     */
    public function findForAdmin(int $page, int $limit, ?string $status = null, ?string $search = null): array
    {
        $qb = $this->adminQb($status, $search)
            ->leftJoin('b.city', 'c')->addSelect('c')
            ->orderBy('b.createdAt', 'DESC')
            ->setFirstResult(($page - 1) * $limit)
            ->setMaxResults($limit);
        return $qb->getQuery()->getResult();
    }

    public function countForAdmin(?string $status = null, ?string $search = null): int
    {
        return (int) $this->adminQb($status, $search)
            ->select('COUNT(b.id)')->getQuery()->getSingleScalarResult();
    }

    private function adminQb(?string $status, ?string $search): \Doctrine\ORM\QueryBuilder
    {
        $qb = $this->createQueryBuilder('b');
        if ($status !== null && $status !== '') {
            $qb->andWhere('b.status = :status')->setParameter('status', $status);
        }
        if ($search !== null && $search !== '') {
            $qb->andWhere('b.title LIKE :q')->setParameter('q', '%' . $search . '%');
        }
        return $qb;
    }

    /**
     * A representative's own posts, newest first, optionally filtered by status.
     * @return Blog[]
     */
    public function findByRepresentation(\App\Representation\Entity\Representation $rep, int $page = 1, int $limit = 20, ?string $status = null): array
    {
        $qb = $this->createQueryBuilder('b')
            ->leftJoin('b.city', 'c')->addSelect('c')
            ->where('b.representation = :rep')
            ->setParameter('rep', $rep)
            ->orderBy('b.createdAt', 'DESC')
            ->setFirstResult(($page - 1) * $limit)
            ->setMaxResults($limit);
        if ($status !== null && $status !== '') {
            $qb->andWhere('b.status = :status')->setParameter('status', $status);
        }
        return $qb->getQuery()->getResult();
    }

    public function countByRepresentation(\App\Representation\Entity\Representation $rep, ?string $status = null): int
    {
        $qb = $this->createQueryBuilder('b')
            ->select('COUNT(b.id)')
            ->where('b.representation = :rep')
            ->setParameter('rep', $rep);
        if ($status !== null && $status !== '') {
            $qb->andWhere('b.status = :status')->setParameter('status', $status);
        }
        return (int) $qb->getQuery()->getSingleScalarResult();
    }

    /**
     * Draft posts due for automatic publish: scheduled_at reached, and either not
     * in the review workflow or already approved (the review gate wins).
     * @return Blog[]
     */
    public function findDueForPublish(int $now): array
    {
        return $this->createQueryBuilder('b')
            ->where('b.status = :draft')
            ->andWhere('b.scheduledAt IS NOT NULL AND b.scheduledAt <= :now')
            ->andWhere('b.reviewStatus IS NULL OR b.reviewStatus = :approved')
            ->setParameter('draft', Blog::STATUS_DRAFT)
            ->setParameter('now', $now)
            ->setParameter('approved', Blog::REVIEW_APPROVED)
            ->getQuery()->getResult();
    }

    /** @return Blog[] published, newest first */
    public function findPublished(int $page = 1, int $limit = 20, ?string $tag = null, ?int $cityId = null): array
    {
        $qb = $this->createQueryBuilder('b')
            ->leftJoin('b.city', 'c')
            ->addSelect('c')
            ->where('b.status = :status')
            ->setParameter('status', Blog::STATUS_PUBLISHED)
            ->orderBy('b.createdAt', 'DESC')
            ->setFirstResult(($page - 1) * $limit)
            ->setMaxResults($limit);

        $this->applyTagFilter($qb, $tag);
        $this->applyCityFilter($qb, $cityId);

        return $qb->getQuery()->getResult();
    }

    public function countPublished(?string $tag = null, ?int $cityId = null): int
    {
        $qb = $this->createQueryBuilder('b')
            ->select('COUNT(b.id)')
            ->where('b.status = :status')
            ->setParameter('status', Blog::STATUS_PUBLISHED);

        $this->applyTagFilter($qb, $tag);
        $this->applyCityFilter($qb, $cityId);

        return (int) $qb->getQuery()->getSingleScalarResult();
    }

    /**
     * Distinct tag names across published posts with their post counts, honouring
     * the same city scope as the public list. Blog tags are free-form strings in a
     * JSON column, so they are counted in PHP — DQL cannot unnest a JSON array.
     *
     * @return list<array{name: string, count: int}> count DESC, then name ASC
     */
    public function tagFacets(?int $cityId = null): array
    {
        $qb = $this->createQueryBuilder('b')
            ->select('b.tags')
            ->where('b.status = :status')
            ->setParameter('status', Blog::STATUS_PUBLISHED);

        $this->applyCityFilter($qb, $cityId);

        $counts = [];
        foreach ($qb->getQuery()->getArrayResult() as $row) {
            foreach ($row['tags'] ?? [] as $name) {
                if (!is_string($name) || $name === '') {
                    continue;
                }
                $counts[$name] = ($counts[$name] ?? 0) + 1;
            }
        }

        // Ties keep a stable, human-predictable order instead of insertion order.
        uksort($counts, static fn(string $a, string $b) => [$counts[$b], $a] <=> [$counts[$a], $b]);

        return array_map(
            static fn(string $name, int $count) => ['name' => $name, 'count' => $count],
            array_keys($counts),
            array_values($counts)
        );
    }

    /**
     * دامنهٔ یک شهر باید پست‌های همان شهر **و** پست‌های سراسری را ببیند — پست
     * سراسری (city NULL) روی همهٔ دامنه‌ها منتشر است، فقط canonicalش روی دامنهٔ اصلی
     * می‌نشیند. بدون شرط NULL، دامنه‌های شهری محتوای عمومی را از دست می‌دادند.
     */
    private function applyCityFilter(\Doctrine\ORM\QueryBuilder $qb, ?int $cityId): void
    {
        if ($cityId === null) {
            return;
        }

        $qb->andWhere('b.city = :cityId OR b.city IS NULL')
           ->setParameter('cityId', $cityId);
    }

    private function applyTagFilter(\Doctrine\ORM\QueryBuilder $qb, ?string $tag): void
    {
        if ($tag === null || $tag === '') {
            return;
        }
        // Blog.tags is a JSON array of tag names; match exact name membership.
        // Doctrine's JsonType writes this column with plain json_encode, so Persian
        // names are stored \uXXXX-escaped. MariaDB's JSON_CONTAINS compares the two
        // documents without normalizing those escapes, so the candidate must be
        // encoded identically — JSON_UNESCAPED_UNICODE never matched a single row.
        $qb->andWhere('JSON_CONTAINS(b.tags, :tag) = 1')
           ->setParameter('tag', json_encode($tag));
    }

    public function save(Blog $e, bool $flush = true): void { $this->getEntityManager()->persist($e); if ($flush) $this->getEntityManager()->flush(); }
    public function remove(Blog $e, bool $flush = true): void { $this->getEntityManager()->remove($e); if ($flush) $this->getEntityManager()->flush(); }
}
