<?php

namespace App\Appointment\Booking\Controller;

use App\Appointment\Booking\Entity\AppointmentHold;
use App\Appointment\Booking\Entity\AppointmentSegment;
use App\Appointment\Booking\Repository\AppointmentHoldRepository;
use App\Appointment\Booking\Repository\AppointmentSegmentRepository;
use App\Appointment\Booking\Service\BookingService;
use App\Appointment\Booking\Service\HoldService;
use App\Appointment\Entity\Appointment;
use App\Auth\Repository\UserRepository;
use App\Doctor\Repository\DoctorRepository;
use App\Pricing\Entity\PriceSnapshot;
use App\Pricing\Service\PriceSnapshotService;
use App\Package\Service\PackageConsumptionService;
use App\Policy\Service\BookingPolicyGuard;
use App\Pricing\Service\PricingEngine;
use App\Appointment\Plan\Service\AppointmentPlanBuilder;
use App\Auth\Entity\User;
use App\Branch\Service\BranchResolver;
use App\ClinicService\Entity\ServiceItem;
use App\ClinicService\Repository\ServiceItemRepository;
use App\Resource\Entity\ClinicResource;
use App\Resource\Repository\ClinicResourceRepository;
use App\Shared\Constant\ErrorCodes;
use App\Shared\Controller\BaseController;
use App\Shared\Event\DomainEventPublisher;
use App\Shared\Event\DomainEvents;
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;

/**
 * سه مرحلهٔ «جستجو → رزرو موقت → ثبت نهایی» روی چند منبع (بند ۱۱ مستند).
 */
#[OA\Tag(name: 'Appointment Booking')]
#[IsGranted('IS_AUTHENTICATED_FULLY')]
class BookingController extends BaseController
{
    public function __construct(
        private readonly AppointmentPlanBuilder    $planner,
        private readonly HoldService               $holds,
        private readonly BookingService            $booking,
        private readonly AppointmentHoldRepository $holdRepo,
        private readonly ServiceItemRepository     $items,
        private readonly ClinicResourceRepository  $resources,
        private readonly DoctorRepository          $doctors,
        private readonly UserRepository            $users,
        private readonly PricingEngine             $pricing,
        private readonly PriceSnapshotService      $snapshots,
        private readonly BranchResolver            $branches,
        private readonly BookingPolicyGuard        $guard,
        private readonly PackageConsumptionService $packages,
        private readonly TenantOwnershipChecker    $ownership,
        private readonly AppointmentSegmentRepository $segments,
        private readonly DomainEventPublisher      $domainEvents,
        private readonly EntityManagerInterface    $em,
    ) {}

    /**
     * پرچم‌هایی که فقط در همین درخواست وجود دارند و جایی ذخیره نمی‌شوند
     * (مثل رضایت والدین که اپراتور همان لحظه می‌گیرد).
     *
     * @param array<string, mixed> $data
     * @return array<string, mixed>
     */
    private function requestFlags(array $data): array
    {
        $flags = [];

        foreach (['has_parental_consent'] as $flag) {
            if (isset($data[$flag])) {
                $flags[$flag] = (bool) $data[$flag];
            }
        }

        if (is_string($data['patient_gender'] ?? null)) {
            $flags['patient_gender'] = $data['patient_gender'];
        }

        return $flags;
    }

    #[Route('/api/v1/appointment-hold', name: 'appointment_hold_create', methods: ['POST'])]
    public function create(#[CurrentUser] User $user, Request $request): JsonResponse
    {
        $data = json_decode($request->getContent(), true);

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

        foreach (['service_uuid', 'branch_uuid'] as $field) {
            if (!is_string($data[$field] ?? null)) {
                return $this->error(ErrorCodes::ERR_VALIDATION_002, sprintf('فیلد %s الزامی است', $field), 422, $field);
            }
        }

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

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

        $address = $this->branches->resolve($user, $data['branch_uuid']);
        $service = $this->requireItem($user, $data['service_uuid']);

        $selected = [];
        foreach (($data['item_uuids'] ?? []) as $itemUuid) {
            if (is_string($itemUuid)) {
                $selected[] = $this->requireItem($user, $itemUuid);
            }
        }

        $plan = $this->planner->build(
            $service,
            $selected,
            $address,
            is_string($data['patient_gender'] ?? null) ? $data['patient_gender'] : null,
        );

        // قوانین وابسته به بیمار پیش از گرفتن صندلی اجرا می‌شوند، نه هنگام ثبت نهایی.
        $this->guard->assertEligible($user, $service, $selected, $address, $this->requestFlags($data));
        $this->guard->assertSpacing($user, $service, $address, (int) $data['start']);

        $assignment = $this->resolveAssignment($user, $data['assignment']);
        $this->assertAssignmentCoversPlan($plan, $assignment);

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

        $hold = $this->holds->hold(
            $user,
            $plan,
            $assignment,
            (int) $data['start'],
            $entityType,
            $entityId,
        );

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

    #[Route('/api/v1/appointment-hold/{uuid}', name: 'appointment_hold_release', methods: ['DELETE'])]
    public function release(#[CurrentUser] User $user, string $uuid): JsonResponse
    {
        $hold = $this->requireHold($user, $uuid);

        if ($hold->isConfirmed()) {
            return $this->error(ErrorCodes::ERR_VALIDATION_001, 'این رزرو ثبت نهایی شده و آزاد نمی‌شود', 422);
        }

        $this->booking->releaseHold($hold);
        $this->em->remove($hold);
        $this->em->flush();

        return $this->success(null);
    }

    /**
     * ثبت نهایی. نوبت با همان قرارداد موجود ساخته می‌شود تا مسیرهای فعلی
     * (`active_slot_key`، رویدادها، پرداخت) دست‌نخورده بمانند — تور ایمنی دوگانه.
     */
    #[Route('/api/v1/appointment-confirm', name: 'appointment_confirm', methods: ['POST'])]
    public function confirm(#[CurrentUser] User $user, Request $request): JsonResponse
    {
        $data = json_decode($request->getContent(), true);

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

        $hold = $this->requireHold($user, $data['hold_uuid']);

        $appointment = $this->makeAppointment($user, $hold, $data);
        $this->em->persist($appointment);
        $this->em->flush();

        $this->booking->confirm($hold, $appointment);

        // فاکتور همین‌جا و با قیمت‌های همین لحظه ثبت می‌شود. اگر بعداً محاسبه می‌شد،
        // تغییر تعرفه بین ثبت و صدور فاکتور، عدد دیگری می‌داد — دقیقاً چیزی که قانون
        // پنجم مستند ممنوع کرده است.
        $snapshot = $this->recordPrice($user, $hold, $appointment, $data);

        return $this->success([
            'appointment_uuid' => $appointment->getUuid(),
            'price_snapshot'   => $snapshot->toArray(),
            'starts_at'        => $hold->getStartsAt(),
            'ends_at'          => $hold->getEndsAt(),
            'assignment'       => $hold->getPayload()['assignment'] ?? [],
        ]);
    }

    /**
     * جابه‌جایی: **اول** رزرو جدید، بعد آزادسازی قدیم.
     *
     * ترتیب عمدی است — اگر رزرو جدید شکست بخورد، نوبت قدیمی دست‌نخورده می‌ماند و
     * بیمار بی‌نوبت نمی‌شود. ترتیب برعکس، در بدترین حالت هر دو را از دست می‌داد.
     */
    /**
     * بخش‌های ثبت‌شدهٔ یک نوبت — عکسِ لحظهٔ رزرو، نه الگوی امروزِ خدمت.
     *
     * فهرست خالی یعنی نوبت اسلاتی است؛ خطا نیست. پنل با همین تفاوت می‌فهمد کدام نوبت
     * را می‌شود منبع‌محور جابه‌جا کرد.
     */
    #[Route('/api/v1/appointment/{uuid}/segments', name: 'appointment_segments', methods: ['GET'])]
    public function segments(#[CurrentUser] User $user, string $uuid): JsonResponse
    {
        $appointment = $this->em->getRepository(Appointment::class)->findOneBy(['uuid' => $uuid]);

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

        if ($appointment === null || !$this->ownership->belongsToPair($entityType, $entityId, $appointment)) {
            return $this->error(ErrorCodes::ERR_NOT_FOUND_001, 'نوبت یافت نشد', 404);
        }

        return $this->success(array_map(
            static fn (AppointmentSegment $s): array => $s->toArray(),
            $this->segments->findForAppointment($appointment),
        ));
    }

    #[Route('/api/v1/appointment/{uuid}/rebook', name: 'appointment_rebook', methods: ['POST'])]
    public function rebook(#[CurrentUser] User $user, string $uuid, Request $request): JsonResponse
    {
        $data = json_decode($request->getContent(), true);

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

        $appointment = $this->em->getRepository(Appointment::class)
            ->findOneBy(['uuid' => $uuid]);

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

        if ($appointment === null || !$this->ownership->belongsToPair($entityType, $entityId, $appointment)) {
            return $this->error(ErrorCodes::ERR_NOT_FOUND_001, 'نوبت یافت نشد', 404);
        }

        $hold = $this->requireHold($user, $data['hold_uuid']);

        $previousStart = $appointment->getSlotStart();

        // رزرو جدید از قبل گرفته شده؛ اینجا فقط تأیید و سپس آزادسازی قدیم.
        $this->booking->confirm($hold, $appointment);
        $released = $this->booking->cancel($appointment);

        // `confirm` و `cancel` هرکدام رویداد خودشان را ثبت کرده‌اند؛ این سومی می‌گوید آن دو
        // یک جابه‌جایی بوده‌اند نه یک لغو و یک رزروِ بی‌ربط. مصرف‌کننده‌ای که فقط
        // `AppointmentCancelled` را بشنود، برای بیماری که هنوز نوبت دارد پیام لغو می‌فرستد.
        $this->domainEvents->recordAndFlush(
            $appointment->getEntityType(),
            $appointment->getEntityId(),
            DomainEvents::APPOINTMENT_RESCHEDULED,
            [
                'appointment_uuid' => $appointment->getUuid(),
                'previous_start'   => $previousStart,
                'new_start'        => $hold->getStartsAt(),
            ],
        );

        return $this->success([
            'appointment_uuid'   => $appointment->getUuid(),
            'released_intervals' => $released,
            'starts_at'          => $hold->getStartsAt(),
        ]);
    }

    /**
     * فاکتور تفکیک‌شده. اگر سرویس پیدا نشد (نوبت ویزیت ساده)، فاکتور با همان
     * `visit_price_rials` موجود ساخته می‌شود؛ خالی گذاشتنش یعنی گزارش مالی یک ردیف
     * کم دارد.
     *
     * @param array<string, mixed> $data
     */
    private function recordPrice(User $user, AppointmentHold $hold, Appointment $appointment, array $data): PriceSnapshot
    {
        if (!is_string($data['service_uuid'] ?? null) || !is_string($data['branch_uuid'] ?? null)) {
            return $this->snapshots->recordFlatVisit($appointment, (int) $appointment->getVisitPriceRials());
        }

        $service = $this->requireItem($user, $data['service_uuid']);
        $address = $this->branches->resolve($user, $data['branch_uuid']);

        $items = [];
        foreach (($data['item_uuids'] ?? []) as $itemUuid) {
            if (is_string($itemUuid)) {
                $items[] = $this->requireItem($user, $itemUuid);
            }
        }

        $quote = $this->pricing->quote(
            $service,
            $items,
            $address,
            $hold->getStartsAt(),
            is_array($data['policy'] ?? null) ? $data['policy'] : [],
            // پروندهٔ بیمار در همین محیط — پکیج کلینیک الف در کلینیک ب معنا ندارد.
            $this->packages->patientRecordFor($appointment),
        );

        return $this->snapshots->record($appointment, $quote);
    }

    /**
     * هر نیازمندی باید در `assignment` منبع داشته باشد. بدون این، رزرو موقت
     * می‌توانست نصفِ منابع لازم را بگیرد و بقیه هنگام حضور بیمار کم بیاید.
     *
     * @param array<string, list<ClinicResource>> $assignment
     */
    private function assertAssignmentCoversPlan(\App\Appointment\Plan\ValueObject\AppointmentPlan $plan, array $assignment): void
    {
        foreach ($plan->segments as $segment) {
            foreach ($segment->requirements as $requirement) {
                $given = count($assignment[$requirement->role] ?? []);

                if ($given < $requirement->count) {
                    throw new AppException(
                        ErrorCodes::ERR_VALIDATION_001,
                        sprintf('برای نقش «%s» منبع کافی انتخاب نشده است', $requirement->roleName),
                        422,
                        'assignment',
                    );
                }
            }
        }
    }

    /**
     * @param array<array-key, mixed> $raw
     * @return array<string, list<ClinicResource>>
     */
    private function resolveAssignment(User $user, array $raw): array
    {
        [$entityType, $entityId] = $this->branches->pair($user);
        $assignment = [];

        foreach ($raw as $role => $uuids) {
            // کلیدِ عددی در JSON یعنی آرایه فرستاده‌اند نه شیء؛ نقش باید نام داشته باشد.
            if (!is_array($uuids) || !is_string($role) || $role === '') {
                throw new AppException(ErrorCodes::ERR_VALIDATION_001, 'ساختار assignment نامعتبر است', 422, 'assignment');
            }

            foreach ($uuids as $resourceUuid) {
                if (!is_string($resourceUuid)) {
                    throw new AppException(ErrorCodes::ERR_VALIDATION_001, 'uuid منبع نامعتبر است', 422, 'assignment');
                }

                $resource = $this->resources->findByUuid($resourceUuid);

                if ($resource === null || !$this->ownership->belongsToPair($entityType, $entityId, $resource)) {
                    throw new AppException(ErrorCodes::ERR_NOT_FOUND_001, 'منبع یافت نشد', 404);
                }

                $assignment[$role][] = $resource;
            }
        }

        return $assignment;
    }

    /**
     * نوبت با همان سازندهٔ موجود ساخته می‌شود، پس `active_slot_key` و رویدادها و
     * مسیر پرداخت دقیقاً مثل قبل کار می‌کنند. اشغال چندمنبعی **کنار** آن می‌نشیند،
     * نه به‌جایش — تور ایمنی دوگانه‌ای که خودِ تسک خواسته است.
     *
     * @param array<string, mixed> $data
     */
    private function makeAppointment(User $user, AppointmentHold $hold, array $data): Appointment
    {
        if (!is_string($data['doctor_uuid'] ?? null)) {
            throw new AppException(ErrorCodes::ERR_VALIDATION_002, 'فیلد doctor_uuid الزامی است', 422, 'doctor_uuid');
        }

        $doctor = $this->doctors->findOneBy(['uuid' => $data['doctor_uuid']]);

        if ($doctor === null) {
            throw new AppException(ErrorCodes::ERR_NOT_FOUND_001, 'پزشک یافت نشد', 404);
        }

        // بیمار پیش‌فرض خودِ کاربر است؛ منشی می‌تواند برای شخص دیگری ثبت کند.
        $patient = $user;

        if (is_string($data['patient_uuid'] ?? null)) {
            $found = $this->users->findOneBy(['uuid' => $data['patient_uuid']]);

            if ($found === null) {
                throw new AppException(ErrorCodes::ERR_NOT_FOUND_001, 'بیمار یافت نشد', 404);
            }

            $patient = $found;
        }

        $appointment = new Appointment($doctor, $patient, $hold->getStartsAt(), $hold->getEndsAt());
        $appointment->assignTenantPair($hold->getEntityType(), $hold->getEntityId());

        return $appointment;
    }

    private function requireHold(User $user, string $uuid): AppointmentHold
    {
        $hold = $this->holdRepo->findByUuid($uuid);
        [$entityType, $entityId] = $this->branches->pair($user);

        // رزرو کاربر دیگر ۴۰۴ می‌گیرد، نه ۴۰۳: وجودش نباید لو برود.
        if ($hold === null
            || $hold->getUser()->getId() !== $user->getId()
            || !$this->ownership->belongsToPair($entityType, $entityId, $hold)
        ) {
            throw new AppException(ErrorCodes::ERR_NOT_FOUND_001, 'رزرو موقت یافت نشد', 404);
        }

        return $hold;
    }

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

        if ($item === null
            || $item->getSection()->getEntityType() !== $entityType
            || $item->getSection()->getEntityId() !== $entityId
        ) {
            throw new AppException(ErrorCodes::ERR_NOT_FOUND_001, 'سرویس یافت نشد', 404);
        }

        return $item;
    }
}
