<?php

namespace App\Blog\Controller;

use App\Auth\Entity\User;
use App\Blog\Entity\Blog;
use App\Blog\Repository\BlogRepository;
use App\Location\Repository\CityRepository;
use App\Shared\Constant\ErrorCodes;
use App\Shared\Controller\BaseController;
use App\Shared\Service\FileValidatorService;
use OpenApi\Attributes as OA;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Security\Http\Attribute\CurrentUser;
use Symfony\Component\Security\Http\Attribute\IsGranted;

#[OA\Tag(name: 'Blog')]
class BlogController extends BaseController
{
    public function __construct(
        private readonly BlogRepository       $blogRepo,
        private readonly CityRepository       $cityRepo,
        private readonly FileValidatorService $fileValidator,
        private readonly \App\Blog\Service\BlogWriter $blogWriter,
        private readonly \App\Blog\Service\BlogCacheInvalidator $cacheInvalidator,
        private readonly string               $projectDir,
    ) {}

    /**
     * city_id ورودی ادمین را به Entity تبدیل می‌کند.
     * مقدار خالی/صفر/null یعنی «سراسری» و عمداً به null نگاشت می‌شود.
     *
     * @throws \App\Shared\Exception\AppException وقتی شناسهٔ شهر نامعتبر باشد
     */
    private function resolveCity(mixed $cityId): ?\App\Location\Entity\City
    {
        if ($cityId === null || $cityId === '' || (int) $cityId === 0) {
            return null;
        }

        $city = $this->cityRepo->find((int) $cityId);
        if ($city === null) {
            throw new \App\Shared\Exception\AppException(ErrorCodes::ERR_VALIDATION_002, 'شهر یافت نشد', 422);
        }

        return $city;
    }

    // ── Public list/detail ────────────────────────────────────────────────────

    #[OA\Get(
        path: '/api/v1/blogs',
        summary: 'List published blog posts (paginated)',
        parameters: [
            new OA\Parameter(name: 'page', in: 'query', required: false, schema: new OA\Schema(type: 'integer', default: 1)),
            new OA\Parameter(name: 'limit', in: 'query', required: false, schema: new OA\Schema(type: 'integer', default: 20, maximum: 50)),
            new OA\Parameter(
                name: 'city_id',
                in: 'query',
                required: false,
                description: 'Scope to one city: returns that city\'s posts plus nationwide posts (city_id IS NULL). Omit to return every published post.',
                schema: new OA\Schema(type: 'integer')
            ),
        ],
        responses: [
            new OA\Response(
                response: 200,
                description: 'Paginated list of published blog posts',
                content: new OA\JsonContent(
                    properties: [
                        new OA\Property(property: 'success', type: 'boolean', example: true),
                        new OA\Property(property: 'data', type: 'array', items: new OA\Items(type: 'object')),
                        new OA\Property(
                            property: 'meta',
                            properties: [
                                new OA\Property(property: 'totalRecords', type: 'integer'),
                                new OA\Property(property: 'totalPages', type: 'integer'),
                                new OA\Property(property: 'currentPage', type: 'integer'),
                            ],
                            type: 'object'
                        ),
                    ]
                )
            ),
        ]
    )]
    #[Route('/api/v1/blogs', methods: ['GET'])]
    public function list(Request $request): JsonResponse
    {
        $page   = max(1, (int) $request->query->get('page', 1));
        $limit  = min(50, max(1, (int) $request->query->get('limit', 20)));
        $tag    = $request->query->get('tag') ?: null;
        $cityId = $request->query->get('city_id') !== null
            ? max(1, (int) $request->query->get('city_id'))
            : null;

        $blogs = array_map(
            fn(Blog $b) => $b->toListArray(),
            $this->blogRepo->findPublished($page, $limit, $tag, $cityId)
        );
        $total = $this->blogRepo->countPublished($tag, $cityId);

        return $this->paginated($blogs, $total, $page, $limit);
    }

    #[OA\Get(
        path: '/api/v1/blogs/tags',
        summary: 'Distinct tag names of published posts with post counts',
        parameters: [
            new OA\Parameter(
                name: 'city_id',
                in: 'query',
                required: false,
                description: 'Same scope as GET /api/v1/blogs: that city\'s posts plus nationwide posts (city_id IS NULL). Omit to count every published post.',
                schema: new OA\Schema(type: 'integer')
            ),
        ],
        responses: [
            new OA\Response(
                response: 200,
                description: 'Tag vocabulary of published posts, most used first',
                content: new OA\JsonContent(
                    properties: [
                        new OA\Property(property: 'success', type: 'boolean', example: true),
                        new OA\Property(
                            property: 'data',
                            type: 'array',
                            items: new OA\Items(
                                properties: [
                                    new OA\Property(property: 'name', type: 'string', example: 'چشم و گوش'),
                                    new OA\Property(property: 'count', type: 'integer', example: 16),
                                ],
                                type: 'object'
                            )
                        ),
                    ]
                )
            ),
        ]
    )]
    #[Route('/api/v1/blogs/tags', methods: ['GET'])]
    public function tags(Request $request): JsonResponse
    {
        $cityId = $request->query->get('city_id') !== null
            ? max(1, (int) $request->query->get('city_id'))
            : null;

        return $this->success($this->blogRepo->tagFacets($cityId));
    }

    #[OA\Get(
        path: '/api/v1/blog/{slug}',
        summary: 'Get a published blog post by slug or UUID',
        parameters: [
            new OA\Parameter(
                name: 'slug',
                in: 'path',
                required: true,
                description: 'Blog slug or UUID',
                schema: new OA\Schema(type: 'string')
            ),
            new OA\Parameter(
                name: 'city_id',
                in: 'query',
                required: false,
                description: 'Domain scope of the caller. A post that belongs to another city is 404 here, exactly as it is absent from GET /api/v1/blogs?city_id=…. Nationwide posts (city IS NULL) are always returned. Omit on the main domain to read any post.',
                schema: new OA\Schema(type: 'integer')
            ),
        ],
        responses: [
            new OA\Response(
                response: 200,
                description: 'Blog post detail',
                content: new OA\JsonContent(
                    properties: [
                        new OA\Property(property: 'success', type: 'boolean', example: true),
                        new OA\Property(
                            property: 'data',
                            properties: [
                                new OA\Property(property: 'data', type: 'object'),
                            ],
                            type: 'object'
                        ),
                    ]
                )
            ),
            new OA\Response(response: 404, description: 'Blog post not found, not published, or owned by another city'),
        ]
    )]
    #[Route('/api/v1/blog/{slug}', methods: ['GET'])]
    public function detail(string $slug, Request $request): JsonResponse
    {
        $blog = $this->blogRepo->findBySlug($slug) ?? $this->blogRepo->findByUuid($slug);
        if ($blog === null || $blog->getStatus() !== Blog::STATUS_PUBLISHED) {
            return $this->error(ErrorCodes::ERR_NOT_FOUND_001, 'مقاله یافت نشد', 404);
        }

        // یک پستِ شهریافته فقط روی دامنهٔ خودش وجود دارد. بدون این شرط، پستی که از
        // لیستِ دامنه حذف شده بود همچنان با URL مستقیم روی هر دامنه‌ای ۲۰۰ می‌گرفت و
        // همان محتوا روی چند دامنه تکرار می‌شد. پست سراسری (city IS NULL) استثناست.
        $cityId = $request->query->get('city_id');
        if ($cityId !== null && $cityId !== '' && !$this->isVisibleOnCity($blog, (int) $cityId)) {
            return $this->error(ErrorCodes::ERR_NOT_FOUND_001, 'مقاله یافت نشد', 404);
        }

        return $this->success(['data' => $blog->toArray()]);
    }

    /** آینهٔ BlogRepository::applyCityFilter — پست همان شهر یا پست سراسری. */
    private function isVisibleOnCity(Blog $blog, int $cityId): bool
    {
        $owner = $blog->getCity();

        return $owner === null || $owner->getId() === $cityId;
    }

    // ── Admin CRUD ────────────────────────────────────────────────────────────

    #[OA\Get(
        path: '/api/v1/admin/blogs',
        summary: 'List blog posts of ALL statuses for the admin panel (draft/published/archived)',
        security: [['bearerAuth' => []]],
        parameters: [
            new OA\Parameter(name: 'page', in: 'query', required: false, schema: new OA\Schema(type: 'integer', default: 1)),
            new OA\Parameter(name: 'limit', in: 'query', required: false, schema: new OA\Schema(type: 'integer', default: 20, maximum: 50)),
            new OA\Parameter(name: 'status', in: 'query', required: false, schema: new OA\Schema(type: 'string', enum: ['draft', 'published', 'archived'])),
            new OA\Parameter(name: 'search', in: 'query', required: false, schema: new OA\Schema(type: 'string')),
        ],
        responses: [new OA\Response(response: 200, description: 'Paginated blog list (all statuses)')]
    )]
    #[IsGranted('ROLE_ADMIN')]
    #[Route('/api/v1/admin/blogs', methods: ['GET'])]
    public function adminList(Request $request): JsonResponse
    {
        $page   = max(1, (int) $request->query->get('page', 1));
        $limit  = min(50, max(1, (int) $request->query->get('limit', 20)));
        $status = $request->query->get('status') ?: null;
        $search = $request->query->get('search') ?: null;

        $items = array_map(
            fn(Blog $b) => $b->toListArray(),
            $this->blogRepo->findForAdmin($page, $limit, $status, $search)
        );
        $total = $this->blogRepo->countForAdmin($status, $search);

        return $this->paginated($items, $total, $page, $limit);
    }


    #[OA\Get(
        path: '/api/v1/admin/blog/{uuid}',
        summary: 'Get one blog post of ANY status for the admin edit form',
        security: [['bearerAuth' => []]],
        parameters: [
            new OA\Parameter(name: 'uuid', in: 'path', required: true, schema: new OA\Schema(type: 'string', format: 'uuid')),
        ],
        responses: [
            new OA\Response(
                response: 200,
                description: 'Blog detail of any status (draft/published/archived)',
                content: new OA\JsonContent(
                    properties: [
                        new OA\Property(property: 'success', type: 'boolean', example: true),
                        new OA\Property(
                            property: 'data',
                            properties: [
                                new OA\Property(property: 'data', type: 'object'),
                            ],
                            type: 'object'
                        ),
                    ]
                )
            ),
            new OA\Response(response: 401, description: 'Unauthorized'),
            new OA\Response(response: 403, description: 'Forbidden — admin role required'),
            new OA\Response(response: 404, description: 'Blog post not found'),
        ]
    )]
    #[IsGranted('ROLE_ADMIN')]
    // requirement اجباری است: این route قبل از /api/v1/admin/blog/review-queue ثبت
    // می‌شود و بدون الگوی uuid، آن مسیر را با uuid="review-queue" می‌دزدید.
    #[Route('/api/v1/admin/blog/{uuid}', methods: ['GET'], requirements: ['uuid' => '[0-9a-fA-F-]{36}'])]
    public function adminDetail(string $uuid): JsonResponse
    {
        // برخلاف detail() عمومی اینجا هیچ فیلتر status/city ای نیست: فرم ویرایش باید
        // پیش‌نویس و آرشیو را هم کامل بارگذاری کند. شکل پاسخ عمداً همان double-nested
        // اندپوینت عمومی است تا مصرف‌کنندهٔ فعلی (data.data.data) دست‌نخورده بماند.
        $blog = $this->blogRepo->findByUuid($uuid);
        if ($blog === null) {
            return $this->error(ErrorCodes::ERR_NOT_FOUND_001, 'مقاله یافت نشد', 404);
        }

        return $this->success(['data' => $blog->toArray()]);
    }

    #[OA\Post(
        path: '/api/v1/blog',
        summary: 'Create a new blog post (ROLE_ADMIN, or ROLE_IMPORTER as a pending-review draft)',
        security: [['bearerAuth' => []]],
        requestBody: new OA\RequestBody(
            required: true,
            content: new OA\JsonContent(
                required: ['title', 'body'],
                properties: [
                    new OA\Property(property: 'title', type: 'string'),
                    new OA\Property(property: 'body', type: 'string'),
                    new OA\Property(property: 'summary', type: 'string', nullable: true),
                    new OA\Property(property: 'tags', type: 'array', items: new OA\Items(type: 'string'), nullable: true),
                    new OA\Property(property: 'status', type: 'string', enum: ['draft', 'published'], nullable: true),
                    new OA\Property(property: 'city_id', type: 'integer', nullable: true, description: 'City this post belongs to. Omit or null for a nationwide post.'),
                ]
            )
        ),
        responses: [
            new OA\Response(
                response: 201,
                description: 'Blog post created',
                content: new OA\JsonContent(
                    properties: [
                        new OA\Property(property: 'success', type: 'boolean', example: true),
                        new OA\Property(
                            property: 'data',
                            properties: [
                                new OA\Property(property: 'data', type: 'object'),
                            ],
                            type: 'object'
                        ),
                    ]
                )
            ),
            new OA\Response(response: 401, description: 'Unauthorized'),
            new OA\Response(response: 403, description: 'Forbidden — ROLE_ADMIN or ROLE_IMPORTER required'),
            new OA\Response(
                response: 422,
                description: 'Validation error',
                content: new OA\JsonContent(
                    properties: [
                        new OA\Property(property: 'success', type: 'boolean', example: false),
                        new OA\Property(
                            property: 'errors',
                            type: 'array',
                            items: new OA\Items(
                                properties: [
                                    new OA\Property(property: 'code', type: 'string'),
                                    new OA\Property(property: 'message', type: 'string'),
                                ]
                            )
                        ),
                    ]
                )
            ),
        ]
    )]
    #[Route('/api/v1/blog', methods: ['POST'])]
    public function create(Request $request, #[CurrentUser] User $user): JsonResponse
    {
        // ROLE_IMPORTER هم مجاز است: خط تولید محتوا (content/ در کرالر) با همان کاربر
        // سیستمیِ ایمپورت لاگین می‌کند و پیش‌نویس می‌سازد. پیش‌نویس هیچ‌وقت منتشر
        // نمی‌شود — تأیید (review) همچنان فقط ROLE_ADMIN است.
        $isAdmin = $this->isGranted('ROLE_ADMIN');
        if (!$isAdmin && !$this->isGranted('ROLE_IMPORTER')) {
            return $this->error(ErrorCodes::ERR_FORBIDDEN_001, 'دسترسی به این منبع مجاز نیست', 403);
        }

        $data  = json_decode($request->getContent(), true) ?? [];
        if ((isset($data['title']) && !is_string($data['title'])) || (isset($data['body']) && !is_string($data['body']))) {
            return $this->error(ErrorCodes::ERR_VALIDATION_002, 'title و body باید رشته باشند', 422);
        }
        $title = trim((string) ($data['title'] ?? ''));
        $body  = trim((string) ($data['body'] ?? ''));

        if (empty($title) || empty($body)) {
            return $this->error(ErrorCodes::ERR_VALIDATION_002, 'title و body الزامی است', 422);
        }

        // Idempotency for the content pipeline: a topic_slug is a stable (specialty,
        // angle) key. A re-run must not create a duplicate — return the existing post.
        $topicSlug = isset($data['topic_slug']) ? trim((string) $data['topic_slug']) : '';
        if ($topicSlug !== '') {
            $existing = $this->blogRepo->findByTopicSlug($topicSlug);
            if ($existing !== null) {
                return $this->success(['data' => $existing->toArray()], 200);
            }
        }

        $blog = new Blog($user, $title, $body);
        if (!empty($data['summary']))   $blog->setSummary($data['summary']);
        if (!empty($data['tags']))      $blog->setTags((array)$data['tags']);
        if (!empty($data['sources']))   $blog->setSources((array)$data['sources']);
        if (!empty($data['status']))    $blog->setStatus($data['status']);
        if (!empty($data['image_url'])) $blog->setImageUrl($data['image_url']);
        if ($topicSlug !== '')          $blog->setTopicSlug($topicSlug);
        // review_status: null means "manual admin post". The pipeline sends
        // "pending_review" so the post enters the doctor review queue.
        if (!empty($data['review_status'])) $blog->setReviewStatus($data['review_status']);
        // ROLE_IMPORTER هرگز نباید بتواند مستقیم منتشر کند: هرچه در بدنه بفرستد،
        // خروجی‌اش پیش‌نویسِ در انتظار بررسی است. بدون این، مجازکردن کرالر روی این
        // اندپوینت یعنی دور زدن کاملِ گیتِ تأیید پزشک.
        if (!$isAdmin) {
            $blog->setStatus(Blog::STATUS_DRAFT);
            $blog->setReviewStatus(Blog::REVIEW_PENDING);
        }
        // نبودِ city_id یعنی سراسری — پس همیشه اعمال می‌شود، نه فقط وقتی مقدار دارد.
        $blog->setCity($this->resolveCity($data['city_id'] ?? null));
        $this->blogWriter->applySeoFields($blog, $data);

        // Ensure slug uniqueness
        if ($this->blogRepo->findBySlug($blog->getSlug()) !== null) {
            $blog->setSlug($blog->getSlug() . '-' . substr(uniqid(), -4));
        }

        $this->blogRepo->save($blog);
        $this->cacheInvalidator->invalidate($blog);

        return $this->success(['data' => $blog->toArray()], 201);
    }

    #[OA\Patch(
        path: '/api/v1/blog/{uuid}',
        summary: 'Update a blog post (admin only)',
        security: [['bearerAuth' => []]],
        requestBody: new OA\RequestBody(
            required: true,
            content: new OA\JsonContent(
                properties: [
                    new OA\Property(property: 'title', type: 'string', nullable: true),
                    new OA\Property(property: 'body', type: 'string', nullable: true),
                    new OA\Property(property: 'summary', type: 'string', nullable: true),
                    new OA\Property(property: 'tags', type: 'array', items: new OA\Items(type: 'string'), nullable: true),
                    new OA\Property(property: 'status', type: 'string', enum: ['draft', 'published'], nullable: true),
                    new OA\Property(property: 'city_id', type: 'integer', nullable: true, description: 'City this post belongs to. Omit or null for a nationwide post.'),
                ]
            )
        ),
        parameters: [
            new OA\Parameter(
                name: 'uuid',
                in: 'path',
                required: true,
                schema: new OA\Schema(type: 'string', format: 'uuid')
            ),
        ],
        responses: [
            new OA\Response(
                response: 200,
                description: 'Blog post updated',
                content: new OA\JsonContent(
                    properties: [
                        new OA\Property(property: 'success', type: 'boolean', example: true),
                        new OA\Property(
                            property: 'data',
                            properties: [
                                new OA\Property(property: 'data', type: 'object'),
                            ],
                            type: 'object'
                        ),
                    ]
                )
            ),
            new OA\Response(response: 401, description: 'Unauthorized'),
            new OA\Response(response: 403, description: 'Forbidden — admin role required'),
            new OA\Response(response: 404, description: 'Blog post not found'),
        ]
    )]
    #[IsGranted('ROLE_ADMIN')]
    #[Route('/api/v1/blog/{uuid}', methods: ['PATCH'])]
    public function update(string $uuid, Request $request): JsonResponse
    {
        $blog = $this->blogRepo->findByUuid($uuid);
        if ($blog === null) {
            return $this->error(ErrorCodes::ERR_NOT_FOUND_001, 'مقاله یافت نشد', 404);
        }

        $data = json_decode($request->getContent(), true) ?? [];
        if (array_key_exists('title', $data))   $blog->setTitle($data['title']);
        if (array_key_exists('body', $data))    $blog->setBody($data['body']);
        if (array_key_exists('summary', $data)) $blog->setSummary($data['summary']);
        if (array_key_exists('tags', $data))      $blog->setTags((array)$data['tags']);
        if (array_key_exists('status', $data))    $blog->setStatus($data['status']);
        if (array_key_exists('image_url', $data)) $blog->setImageUrl($data['image_url'] ?: null);
        // PATCH: فقط وقتی صریحاً فرستاده شد تغییر کند. ارسال null یعنی «سراسری‌اش کن».
        if (array_key_exists('city_id', $data))   $blog->setCity($this->resolveCity($data['city_id']));
        $this->blogWriter->applySeoFields($blog, $data);

        $this->blogRepo->save($blog);
        $this->cacheInvalidator->invalidate($blog);

        return $this->success(['data' => $blog->toArray()]);
    }

    // ── Medical-review gate ───────────────────────────────────────────────────

    #[OA\Get(
        path: '/api/v1/admin/blog/review-queue',
        summary: 'List blog drafts awaiting doctor review (ROLE_ADMIN or ROLE_IMPORTER)',
        security: [['bearerAuth' => []]],
        parameters: [
            new OA\Parameter(name: 'page', in: 'query', required: false, schema: new OA\Schema(type: 'integer', default: 1)),
            new OA\Parameter(name: 'limit', in: 'query', required: false, schema: new OA\Schema(type: 'integer', default: 20, maximum: 50)),
        ],
        responses: [
            new OA\Response(response: 200, description: 'Paginated review queue'),
            new OA\Response(response: 401, description: 'Unauthorized'),
            new OA\Response(response: 403, description: 'Forbidden — ROLE_ADMIN or ROLE_IMPORTER required'),
        ]
    )]
    #[Route('/api/v1/admin/blog/review-queue', methods: ['GET'])]
    public function reviewQueue(Request $request): JsonResponse
    {
        // خواندنی است، پس ROLE_IMPORTER هم مجاز است تا پنل کرالر صف را نشان دهد.
        // تصمیم تأیید/رد (review) همچنان فقط ROLE_ADMIN.
        if (!$this->isGranted('ROLE_ADMIN') && !$this->isGranted('ROLE_IMPORTER')) {
            return $this->error(ErrorCodes::ERR_FORBIDDEN_001, 'دسترسی به این منبع مجاز نیست', 403);
        }

        $page  = max(1, (int) $request->query->get('page', 1));
        $limit = min(50, max(1, (int) $request->query->get('limit', 20)));

        $items = array_map(
            fn(Blog $b) => $b->toListArray(),
            $this->blogRepo->findByReviewStatus(Blog::REVIEW_PENDING, $page, $limit)
        );
        $total = $this->blogRepo->countByReviewStatus(Blog::REVIEW_PENDING);

        return $this->paginated($items, $total, $page, $limit);
    }

    #[OA\Post(
        path: '/api/v1/admin/blog/{uuid}/review',
        summary: 'Approve or reject a blog draft (admin only). Approving may publish it.',
        security: [['bearerAuth' => []]],
        requestBody: new OA\RequestBody(
            required: true,
            content: new OA\JsonContent(
                required: ['decision'],
                properties: [
                    new OA\Property(property: 'decision', type: 'string', enum: ['approved', 'rejected']),
                    new OA\Property(property: 'note', type: 'string', nullable: true, description: 'Reviewer note (required message on rejection).'),
                    new OA\Property(property: 'publish', type: 'boolean', nullable: true, description: 'On approval, publish immediately (default true).'),
                ]
            )
        ),
        parameters: [
            new OA\Parameter(name: 'uuid', in: 'path', required: true, schema: new OA\Schema(type: 'string', format: 'uuid')),
        ],
        responses: [
            new OA\Response(response: 200, description: 'Review decision recorded'),
            new OA\Response(response: 401, description: 'Unauthorized'),
            new OA\Response(response: 403, description: 'Forbidden — admin role required'),
            new OA\Response(response: 404, description: 'Blog post not found'),
            new OA\Response(response: 422, description: 'Invalid decision'),
        ]
    )]
    #[IsGranted('ROLE_ADMIN')]
    #[Route('/api/v1/admin/blog/{uuid}/review', methods: ['POST'])]
    public function review(string $uuid, Request $request, #[CurrentUser] User $user): JsonResponse
    {
        $blog = $this->blogRepo->findByUuid($uuid);
        if ($blog === null) {
            return $this->error(ErrorCodes::ERR_NOT_FOUND_001, 'مقاله یافت نشد', 404);
        }

        $data     = json_decode($request->getContent(), true) ?? [];
        $decision = $data['decision'] ?? null;
        $note     = isset($data['note']) ? trim((string) $data['note']) : null;

        if (!in_array($decision, [Blog::REVIEW_APPROVED, Blog::REVIEW_REJECTED], true)) {
            return $this->error(ErrorCodes::ERR_VALIDATION_002, 'decision باید approved یا rejected باشد', 422);
        }
        if ($decision === Blog::REVIEW_REJECTED && ($note === null || $note === '')) {
            return $this->error(ErrorCodes::ERR_VALIDATION_002, 'برای رد مقاله، درج دلیل الزامی است', 422);
        }

        $blog->applyReview($decision, $user, $note);

        // Approving publishes by default; rejecting keeps the post as a draft.
        if ($decision === Blog::REVIEW_APPROVED && ($data['publish'] ?? true)) {
            $blog->setStatus(Blog::STATUS_PUBLISHED);
        } elseif ($decision === Blog::REVIEW_REJECTED) {
            $blog->setStatus(Blog::STATUS_DRAFT);
        }

        $this->blogRepo->save($blog);
        $this->cacheInvalidator->invalidate($blog);

        return $this->success(['data' => $blog->toArray()]);
    }

    #[OA\Delete(
        path: '/api/v1/blog/{uuid}',
        summary: 'Delete a blog post (admin only)',
        security: [['bearerAuth' => []]],
        parameters: [
            new OA\Parameter(
                name: 'uuid',
                in: 'path',
                required: true,
                schema: new OA\Schema(type: 'string', format: 'uuid')
            ),
        ],
        responses: [
            new OA\Response(
                response: 200,
                description: 'Blog post deleted',
                content: new OA\JsonContent(
                    properties: [
                        new OA\Property(property: 'success', type: 'boolean', example: true),
                        new OA\Property(
                            property: 'data',
                            properties: [
                                new OA\Property(property: 'message', type: 'string'),
                            ],
                            type: 'object'
                        ),
                    ]
                )
            ),
            new OA\Response(response: 401, description: 'Unauthorized'),
            new OA\Response(response: 403, description: 'Forbidden — admin role required'),
            new OA\Response(response: 404, description: 'Blog post not found'),
        ]
    )]
    #[IsGranted('ROLE_ADMIN')]
    #[Route('/api/v1/blog/{uuid}', methods: ['DELETE'])]
    public function delete(string $uuid): JsonResponse
    {
        $blog = $this->blogRepo->findByUuid($uuid);
        if ($blog === null) {
            return $this->error(ErrorCodes::ERR_NOT_FOUND_001, 'مقاله یافت نشد', 404);
        }

        // باطل‌سازی قبل از حذف: بعد از remove، Doctrine آبجکت را detach می‌کند و
        // اتکا به مقادیر باقی‌مانده در حافظه شکننده است.
        $this->cacheInvalidator->invalidate($blog);
        $this->blogRepo->remove($blog);

        return $this->success(['message' => 'مقاله با موفقیت حذف شد']);
    }

    // ── Image upload ──────────────────────────────────────────────────────────

    #[OA\Post(
        path: '/file/upload/clinic_pro/blog/field_image',
        summary: 'Upload a blog post image (admin only)',
        security: [['bearerAuth' => []]],
        requestBody: new OA\RequestBody(
            required: true,
            content: new OA\MediaType(
                mediaType: 'multipart/form-data',
                schema: new OA\Schema(
                    required: ['file'],
                    properties: [
                        new OA\Property(property: 'file', type: 'string', format: 'binary'),
                        new OA\Property(property: 'blog_uuid', type: 'string', format: 'uuid', nullable: true),
                    ]
                )
            )
        ),
        responses: [
            new OA\Response(
                response: 200,
                description: 'Image uploaded',
                content: new OA\JsonContent(
                    properties: [
                        new OA\Property(property: 'success', type: 'boolean', example: true),
                        new OA\Property(
                            property: 'data',
                            properties: [
                                new OA\Property(property: 'image_url', type: 'string'),
                                new OA\Property(property: 'filename', type: 'string'),
                            ],
                            type: 'object'
                        ),
                    ]
                )
            ),
            new OA\Response(response: 401, description: 'Unauthorized'),
            new OA\Response(response: 403, description: 'Forbidden — admin role required'),
            new OA\Response(
                response: 422,
                description: 'No file provided or invalid file',
                content: new OA\JsonContent(
                    properties: [
                        new OA\Property(property: 'success', type: 'boolean', example: false),
                        new OA\Property(
                            property: 'errors',
                            type: 'array',
                            items: new OA\Items(
                                properties: [
                                    new OA\Property(property: 'code', type: 'string'),
                                    new OA\Property(property: 'message', type: 'string'),
                                ]
                            )
                        ),
                    ]
                )
            ),
        ]
    )]
    #[IsGranted('ROLE_ADMIN')]
    #[Route('/file/upload/clinic_pro/blog/field_image', methods: ['POST'])]
    public function uploadImage(Request $request): JsonResponse
    {
        $file     = $request->files->get('file');
        $blogUuid = $request->request->get('blog_uuid', '');

        if ($file === null) {
            return $this->error(ErrorCodes::ERR_FILE_001, 'فایل ارسال نشده است', 422);
        }

        try {
            $safeFilename = $this->fileValidator->validateUploadedFile($file);
        } catch (\App\Shared\Exception\AppException $e) {
            return $this->error($e->getErrorCode(), $e->getMessage(), $e->getHttpStatus());
        }

        $uploadDir = $this->projectDir . '/public/uploads/blogs/';
        if (!is_dir($uploadDir)) mkdir($uploadDir, 0755, true);

        $filename = uniqid('blog_') . '_' . $safeFilename;
        $file->move($uploadDir, $filename);

        $imageUrl  = '/uploads/blogs/' . $filename;
        $imagePath = $uploadDir . $filename;

        if (!empty($blogUuid)) {
            $blog = $this->blogRepo->findByUuid($blogUuid);
            if ($blog !== null) {
                $blog->setImageUrl($imageUrl)->setImagePath($imagePath);
                $this->blogRepo->save($blog);
            }
        }

        return $this->success(['image_url' => $imageUrl, 'filename' => $filename]);
    }
}
