<?php

namespace App\ClinicService\Controller;

use App\Auth\Entity\User;
use App\Branch\Service\BranchResolver;
use App\ClinicService\Entity\CatalogCategory;
use App\ClinicService\Entity\ItemGroup;
use App\ClinicService\Entity\ItemGroupMember;
use App\ClinicService\Entity\ServiceItem;
use App\ClinicService\Entity\ServiceBranchOverride;
use App\ClinicService\Entity\ServiceItemRelation;
use App\ClinicService\Repository\CatalogCategoryRepository;
use App\ClinicService\Repository\ItemGroupMemberRepository;
use App\ClinicService\Repository\ItemGroupRepository;
use App\ClinicService\Repository\ServiceBranchOverrideRepository;
use App\ClinicService\Repository\ServiceItemRelationRepository;
use App\ClinicService\Repository\ServiceItemRepository;
use App\ClinicService\Service\ServiceSelectionValidator;
use App\Shared\Constant\ErrorCodes;
use App\Shared\Controller\BaseController;
use App\Shared\Exception\AppException;
use App\Shared\Tenant\TenantOwnershipChecker;
use Doctrine\ORM\EntityManagerInterface;
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;

/**
 * کاتالوگ نسخهٔ ۲ — دستهٔ درختی، گروه انتخاب، رابطهٔ آیتم‌ها و اعتبارسنجی انتخاب.
 *
 * اندپوینت‌های موجود سرویس ({@see ClinicServiceController}) دست‌نخورده‌اند؛ این کنترلر
 * فقط چیزهای تازه را اضافه می‌کند.
 */
#[OA\Tag(name: 'Clinic Services')]
#[IsGranted('IS_AUTHENTICATED_FULLY')]
class ServiceCatalogController extends BaseController
{
    public function __construct(
        private readonly CatalogCategoryRepository     $categories,
        private readonly ItemGroupRepository           $groups,
        private readonly ItemGroupMemberRepository     $groupMembers,
        private readonly ServiceItemRepository         $items,
        private readonly ServiceItemRelationRepository $relations,
        private readonly ServiceBranchOverrideRepository $overrides,
        private readonly ServiceSelectionValidator     $validator,
        private readonly BranchResolver                $branches,
        private readonly TenantOwnershipChecker        $ownership,
        private readonly EntityManagerInterface        $em,
    ) {}

    // ── دستهٔ درختی ─────────────────────────────────────────────────────────

    #[Route('/api/v1/service-categories/tree', name: 'service_category_tree', methods: ['GET'])]
    public function tree(#[CurrentUser] User $user): JsonResponse
    {
        [$entityType, $entityId] = $this->branches->pair($user);

        // کل درخت با یک کوئری خوانده و در PHP بسته می‌شود.
        $all = $this->categories->findForPair($entityType, $entityId);

        $childrenOf = [];
        foreach ($all as $node) {
            $childrenOf[$node->getParent()?->getId() ?? 0][] = $node;
        }

        return $this->success($this->buildTree($childrenOf, 0));
    }

    /** @return list<array<string, mixed>> */
    private function buildTree(array $childrenOf, int $parentId): array
    {
        return array_map(
            fn (CatalogCategory $node): array => $node->toArray(
                $this->buildTree($childrenOf, (int) $node->getId()),
            ),
            $childrenOf[$parentId] ?? [],
        );
    }

    #[Route('/api/v1/service-category', name: 'service_category_create', methods: ['POST'])]
    public function createCategory(#[CurrentUser] User $user, Request $request): JsonResponse
    {
        $data = json_decode($request->getContent(), true);
        $name = is_array($data) && is_string($data['name'] ?? null) ? trim($data['name']) : '';

        if ($name === '') {
            return $this->error(ErrorCodes::ERR_VALIDATION_002, 'نام دسته الزامی است', 422, 'name');
        }

        [$entityType, $entityId] = $this->branches->pair($user);

        $parent = is_string($data['parent_uuid'] ?? null)
            ? $this->requireCategory($user, $data['parent_uuid'])
            : null;

        $category = new CatalogCategory($entityType, $entityId, $name, $parent);

        if ($category->depth() > CatalogCategory::MAX_DEPTH) {
            return $this->error(
                ErrorCodes::ERR_VALIDATION_001,
                sprintf('عمق دسته‌بندی حداکثر %d سطح است', CatalogCategory::MAX_DEPTH),
                422,
                'parent_uuid',
            );
        }

        if (is_numeric($data['sort_order'] ?? null)) {
            $category->setSortOrder((int) $data['sort_order']);
        }

        $this->em->persist($category);
        $this->em->flush();

        return $this->success($category->toArray(), 201);
    }

    #[Route('/api/v1/service-category/{uuid}', name: 'service_category_update', methods: ['PATCH'])]
    public function updateCategory(#[CurrentUser] User $user, string $uuid, Request $request): JsonResponse
    {
        $data     = json_decode($request->getContent(), true);
        $category = $this->requireCategory($user, $uuid);

        if (!is_array($data)) {
            return $this->error(ErrorCodes::ERR_VALIDATION_001, 'بدنهٔ درخواست نامعتبر است', 422);
        }

        if (is_string($data['name'] ?? null) && trim($data['name']) !== '') {
            $category->setName(trim($data['name']));
        }

        if (is_numeric($data['sort_order'] ?? null)) {
            $category->setSortOrder((int) $data['sort_order']);
        }

        if (array_key_exists('active', $data)) {
            $category->setActive((bool) $data['active']);
        }

        $this->em->flush();

        return $this->success($category->toArray());
    }

    #[Route('/api/v1/service-category/{uuid}', name: 'service_category_delete', methods: ['DELETE'])]
    public function deleteCategory(#[CurrentUser] User $user, string $uuid): JsonResponse
    {
        $category = $this->requireCategory($user, $uuid);

        if ($this->categories->countChildren($category) > 0) {
            return $this->error(ErrorCodes::ERR_VALIDATION_001, 'ابتدا زیردسته‌ها را حذف کنید', 422);
        }

        $this->em->remove($category);
        $this->em->flush();

        return $this->success(null);
    }

    // ── گروه آیتم ───────────────────────────────────────────────────────────

    #[Route('/api/v1/service-item/{uuid}/groups', name: 'service_item_groups', methods: ['GET'])]
    public function listGroups(#[CurrentUser] User $user, string $uuid): JsonResponse
    {
        $service = $this->requireItem($user, $uuid);

        return $this->success(array_map(
            static fn (ItemGroup $g): array => $g->toArray(),
            $this->groups->findForService($service),
        ));
    }

    #[Route('/api/v1/service-item/{uuid}/groups', name: 'service_item_group_create', methods: ['POST'])]
    public function createGroup(#[CurrentUser] User $user, string $uuid, Request $request): JsonResponse
    {
        $data    = json_decode($request->getContent(), true);
        $service = $this->requireItem($user, $uuid);
        $name    = is_array($data) && is_string($data['name'] ?? null) ? trim($data['name']) : '';

        if ($name === '') {
            return $this->error(ErrorCodes::ERR_VALIDATION_002, 'نام گروه الزامی است', 422, 'name');
        }

        $group = new ItemGroup($service, $name);
        $this->applyGroupRange($group, $data);

        if (is_numeric($data['sort_order'] ?? null)) {
            $group->setSortOrder((int) $data['sort_order']);
        }

        $this->em->persist($group);
        $this->em->flush();

        return $this->success($group->toArray(), 201);
    }

    #[Route('/api/v1/item-group/{uuid}', name: 'item_group_update', methods: ['PATCH'])]
    public function updateGroup(#[CurrentUser] User $user, string $uuid, Request $request): JsonResponse
    {
        $data  = json_decode($request->getContent(), true);
        $group = $this->requireGroup($user, $uuid);

        if (!is_array($data)) {
            return $this->error(ErrorCodes::ERR_VALIDATION_001, 'بدنهٔ درخواست نامعتبر است', 422);
        }

        if (is_string($data['name'] ?? null) && trim($data['name']) !== '') {
            $group->setName(trim($data['name']));
        }

        $this->applyGroupRange($group, $data);

        if (is_numeric($data['sort_order'] ?? null)) {
            $group->setSortOrder((int) $data['sort_order']);
        }

        $this->em->flush();

        return $this->success($group->toArray());
    }

    #[Route('/api/v1/item-group/{uuid}', name: 'item_group_delete', methods: ['DELETE'])]
    public function deleteGroup(#[CurrentUser] User $user, string $uuid): JsonResponse
    {
        $this->em->remove($this->requireGroup($user, $uuid));
        $this->em->flush();

        return $this->success(null);
    }

    /** جایگزینی کامل آیتم‌های گروه. */
    #[Route('/api/v1/item-group/{uuid}/items', name: 'item_group_items_replace', methods: ['PUT'])]
    public function replaceGroupItems(#[CurrentUser] User $user, string $uuid, Request $request): JsonResponse
    {
        $data = json_decode($request->getContent(), true);

        if (!is_array($data) || !is_array($data['items'] ?? null)) {
            return $this->error(ErrorCodes::ERR_VALIDATION_002, 'فیلد items الزامی است', 422, 'items');
        }

        $group = $this->requireGroup($user, $uuid);

        // همهٔ آیتم‌ها پیش از هر حذفی حل می‌شوند: uuid نامعتبر در انتهای فهرست نباید
        // اعضای درستِ قبلی را پاک کند و بعد ۴۰۴ بدهد.
        $resolved = [];
        foreach ($data['items'] as $index => $row) {
            $itemUuid = is_array($row) ? ($row['item_uuid'] ?? null) : $row;

            if (!is_string($itemUuid) || $itemUuid === '') {
                return $this->error(ErrorCodes::ERR_VALIDATION_002, 'فیلد item_uuid الزامی است', 422, 'item_uuid');
            }

            $sortOrder  = is_array($row) && is_numeric($row['sort_order'] ?? null) ? (int) $row['sort_order'] : $index;
            $resolved[] = [$this->requireItem($user, $itemUuid), $sortOrder];
        }

        $this->groupMembers->deleteForGroup($group);
        $group->getMembers()->clear();

        foreach ($resolved as [$item, $sortOrder]) {
            $member = new ItemGroupMember($group, $item, $sortOrder);
            $this->em->persist($member);
            $group->getMembers()->add($member);
        }

        $group->touch();
        $this->em->flush();

        return $this->success($group->toArray());
    }

    // ── رابطهٔ آیتم‌ها ───────────────────────────────────────────────────────

    #[Route('/api/v1/service-item/{uuid}/relations', name: 'service_item_relations_replace', methods: ['PUT'])]
    public function replaceRelations(#[CurrentUser] User $user, string $uuid, Request $request): JsonResponse
    {
        $data = json_decode($request->getContent(), true);

        if (!is_array($data) || !is_array($data['relations'] ?? null)) {
            return $this->error(ErrorCodes::ERR_VALIDATION_002, 'فیلد relations الزامی است', 422, 'relations');
        }

        $item     = $this->requireItem($user, $uuid);
        $resolved = [];

        foreach ($data['relations'] as $row) {
            if (!is_array($row) || !is_string($row['related_item_uuid'] ?? null) || !is_string($row['type'] ?? null)) {
                return $this->error(ErrorCodes::ERR_VALIDATION_002, 'related_item_uuid و type الزامی‌اند', 422, 'relations');
            }

            if (!in_array($row['type'], ServiceItemRelation::TYPES, true)) {
                return $this->error(ErrorCodes::ERR_VALIDATION_001, 'نوع رابطه نامعتبر است', 422, 'type');
            }

            $related = $this->requireItem($user, $row['related_item_uuid']);

            if ($related->getId() === $item->getId()) {
                return $this->error(ErrorCodes::ERR_VALIDATION_001, 'آیتم نمی‌تواند با خودش رابطه داشته باشد', 422, 'related_item_uuid');
            }

            if ($row['type'] === ServiceItemRelation::TYPE_REQUIRES && $this->wouldCycle($item, $related)) {
                return $this->error(
                    ErrorCodes::ERR_VALIDATION_001,
                    sprintf('«%s» به‌طور غیرمستقیم پیش‌نیاز «%s» است؛ حلقهٔ پیش‌نیاز مجاز نیست', $related->getName(), $item->getName()),
                    422,
                    'related_item_uuid',
                );
            }

            $resolved[] = [$related, $row['type']];
        }

        $this->relations->deleteForItem($item);

        foreach ($resolved as [$related, $type]) {
            $this->em->persist(new ServiceItemRelation($item, $related, $type));
        }

        $this->em->flush();

        return $this->success(array_map(
            static fn (ServiceItemRelation $r): array => $r->toArray(),
            $this->relations->findForItem($item),
        ));
    }

    /**
     * آیا «`$related` پیش‌نیاز `$item`» حلقه می‌سازد؟ یعنی آیا `$item` از راه زنجیرهٔ
     * پیش‌نیازها به `$related` می‌رسد. بدون این بررسی، «الف نیازمند ب» و «ب نیازمند
     * الف» هر دو ذخیره می‌شدند و اعتبارسنجی انتخاب هرگز راضی نمی‌شد.
     */
    private function wouldCycle(ServiceItem $item, ServiceItem $related): bool
    {
        $stack   = [$related];
        $visited = [];

        while ($stack !== []) {
            $current = array_pop($stack);
            $id      = (int) $current->getId();

            if (isset($visited[$id])) {
                continue;
            }
            $visited[$id] = true;

            if ($id === (int) $item->getId()) {
                return true;
            }

            foreach ($this->relations->findForItem($current) as $relation) {
                if ($relation->getType() === ServiceItemRelation::TYPE_REQUIRES) {
                    $stack[] = $relation->getRelatedItem();
                }
            }
        }

        return false;
    }

    // ── قیمت و مدت اختصاصی شعبه ─────────────────────────────────────────────

    #[Route('/api/v1/service-item/{uuid}/branch-overrides', name: 'service_item_overrides_replace', methods: ['PUT'])]
    public function replaceBranchOverrides(#[CurrentUser] User $user, string $uuid, Request $request): JsonResponse
    {
        $data = json_decode($request->getContent(), true);

        if (!is_array($data) || !is_array($data['overrides'] ?? null)) {
            return $this->error(ErrorCodes::ERR_VALIDATION_002, 'فیلد overrides الزامی است', 422, 'overrides');
        }

        $item     = $this->requireItem($user, $uuid);
        $resolved = [];

        // مثل بقیهٔ PUT های این پروژه: همه‌چیز پیش از هر حذفی حل و اعتبارسنجی می‌شود.
        foreach ($data['overrides'] as $row) {
            if (!is_array($row) || !is_string($row['address_uuid'] ?? null)) {
                return $this->error(ErrorCodes::ERR_VALIDATION_002, 'فیلد address_uuid الزامی است', 422, 'address_uuid');
            }

            foreach (['price_rials', 'solo_duration_minutes', 'additional_duration_minutes'] as $field) {
                $value = $row[$field] ?? null;

                if ($value !== null && (!is_numeric($value) || (int) $value < 0)) {
                    return $this->error(ErrorCodes::ERR_VALIDATION_001, sprintf('%s نامعتبر است', $field), 422, $field);
                }
            }

            $resolved[] = [$this->branches->resolve($user, $row['address_uuid']), $row];
        }

        $this->overrides->deleteForItem($item);

        foreach ($resolved as [$address, $row]) {
            $override = new ServiceBranchOverride($item, $address);

            // `isset()` خودش null را رد می‌کند، پس مقایسهٔ اضافه لازم نیست.
            // `null` یعنی «همان مقدار خودِ سرویس» — صفر نیست.
            $override->setPriceRials(isset($row['price_rials']) ? (int) $row['price_rials'] : null);
            $override->setSoloDurationMinutes(isset($row['solo_duration_minutes']) ? (int) $row['solo_duration_minutes'] : null);
            $override->setAdditionalDurationMinutes(isset($row['additional_duration_minutes']) ? (int) $row['additional_duration_minutes'] : null);

            $this->em->persist($override);
        }

        $this->em->flush();

        return $this->success(array_map(
            static fn (ServiceBranchOverride $o): array => $o->toArray(),
            $this->overrides->findForItem($item),
        ));
    }

    // ── اعتبارسنجی انتخاب ───────────────────────────────────────────────────

    /**
     * مهم‌ترین اندپوینت این تسک: سایت عمومی و پنل هر دو پیش از رفتن به مرحلهٔ انتخاب
     * زمان آن را صدا می‌زنند.
     */
    #[Route('/api/v1/service-selection/validate', name: 'service_selection_validate', methods: ['POST'])]
    public function validateSelection(#[CurrentUser] User $user, Request $request): JsonResponse
    {
        $data = json_decode($request->getContent(), true);

        if (!is_array($data) || !is_array($data['item_uuids'] ?? null)) {
            return $this->error(ErrorCodes::ERR_VALIDATION_002, 'فیلد item_uuids الزامی است', 422, 'item_uuids');
        }

        $selected = [];
        foreach ($data['item_uuids'] as $itemUuid) {
            if (!is_string($itemUuid)) {
                return $this->error(ErrorCodes::ERR_VALIDATION_001, 'item_uuids باید فهرستی از uuid باشد', 422, 'item_uuids');
            }

            // آیتم محیط دیگر ۴۰۴ می‌دهد نه ۴۲۲: وجودش نباید لو برود.
            $selected[] = $this->requireItem($user, $itemUuid);
        }

        $groups = is_string($data['service_uuid'] ?? null)
            ? $this->groups->findForService($this->requireItem($user, $data['service_uuid']))
            : $this->validator->groupsOf($selected);

        $address = is_string($data['branch_uuid'] ?? null)
            ? $this->branches->resolve($user, $data['branch_uuid'])
            : null;

        return $this->success($this->validator->validate($selected, $groups, $address));
    }

    // ── حل uuid با بررسی محیط ───────────────────────────────────────────────

    private function requireCategory(User $user, string $uuid): CatalogCategory
    {
        return $this->owned($user, $this->categories->findByUuid($uuid), 'دسته یافت نشد');
    }

    private function requireGroup(User $user, string $uuid): ItemGroup
    {
        return $this->owned($user, $this->groups->findByUuid($uuid), 'گروه یافت نشد');
    }

    private function requireItem(User $user, string $uuid): ServiceItem
    {
        $item = $this->items->findByUuid($uuid);
        [$entityType, $entityId] = $this->branches->pair($user);

        // ServiceItem جفت محیط خودش را ندارد؛ از بخشش می‌آید.
        if ($item === null
            || $item->getSection()->getEntityType() !== $entityType
            || $item->getSection()->getEntityId() !== $entityId
        ) {
            throw new AppException(ErrorCodes::ERR_NOT_FOUND_001, 'سرویس یافت نشد', 404);
        }

        return $item;
    }

    /**
     * @template T of object
     * @param T|null $entity
     * @return T
     */
    private function owned(User $user, ?object $entity, string $message): object
    {
        [$entityType, $entityId] = $this->branches->pair($user);

        if ($entity === null || !$this->ownership->belongsToPair($entityType, $entityId, $entity)) {
            throw new AppException(ErrorCodes::ERR_NOT_FOUND_001, $message, 404);
        }

        return $entity;
    }

    /** @param array<string, mixed>|null $data */
    private function applyGroupRange(ItemGroup $group, ?array $data): void
    {
        if (!is_array($data)) {
            return;
        }

        $min = array_key_exists('min_select', $data) ? $data['min_select'] : $group->getMinSelect();
        $max = array_key_exists('max_select', $data) ? $data['max_select'] : $group->getMaxSelect();

        try {
            $group->setSelectRange(
                is_numeric($min) ? (int) $min : 0,
                $max === null ? null : (is_numeric($max) ? (int) $max : null),
            );
        } catch (\InvalidArgumentException $e) {
            throw new AppException(ErrorCodes::ERR_VALIDATION_001, 'بازهٔ انتخاب نامعتبر است', 422, 'max_select');
        }
    }
}
