<?php

namespace App\Appointment\Availability\Controller;

use App\Appointment\Availability\Service\AvailabilityEngine;
use App\Appointment\Availability\ValueObject\AvailableSlot;
use App\Appointment\Entity\WeeklySchedule;
use App\Appointment\Plan\Service\AppointmentPlanBuilder;
use App\Appointment\Repository\WeeklyScheduleRepository;
use App\Auth\Entity\User;
use App\Branch\Service\BranchResolver;
use App\ClinicService\Entity\ServiceItem;
use App\ClinicService\Repository\ServiceItemRepository;
use App\Doctor\Entity\DoctorAddress;
use App\Doctor\Repository\DoctorRepository;
use App\Shared\Constant\ErrorCodes;
use App\Shared\Controller\BaseController;
use App\Shared\Exception\AppException;
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;

/**
 * جستجوی وقت چندمنبعی.
 *
 * فقط برای محل‌هایی که صریحاً روی `booking_mode = resource` رفته‌اند. بقیه همان مسیر
 * قبلی (`appointment-slots` / `appointment-service-slots`) را دارند و دست‌نخورده‌اند.
 */
#[OA\Tag(name: 'Appointment Availability')]
#[IsGranted('IS_AUTHENTICATED_FULLY')]
class AvailabilityController extends BaseController
{
    public function __construct(
        private readonly AppointmentPlanBuilder   $planner,
        private readonly AvailabilityEngine       $engine,
        private readonly ServiceItemRepository    $items,
        private readonly WeeklyScheduleRepository $schedules,
        private readonly DoctorRepository         $doctors,
        private readonly BranchResolver           $branches,
        private readonly \App\Course\Repository\TreatmentCourseRepository $courses,
    ) {}

    #[Route('/api/v1/appointment-availability', name: 'appointment_availability', methods: ['POST'])]
    public function search(#[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['from'] ?? null) || !is_numeric($data['to'] ?? null)) {
            return $this->error(ErrorCodes::ERR_VALIDATION_002, 'فیلدهای from و to الزامی‌اند', 422, 'from');
        }

        $from = (int) $data['from'];
        $to   = (int) $data['to'];

        if ($to < $from) {
            return $this->error(ErrorCodes::ERR_VALIDATION_001, 'to باید بعد از from باشد', 422, 'to');
        }

        if (intdiv($to - $from, 86400) + 1 > AvailabilityEngine::MAX_DAYS) {
            return $this->error(
                ErrorCodes::ERR_VALIDATION_001,
                sprintf('بازهٔ درخواستی حداکثر %d روز است', AvailabilityEngine::MAX_DAYS),
                422,
                'to',
            );
        }

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

        $this->assertResourceMode($data['doctor_uuid'] ?? null, $address);

        $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);
        }

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

        $step  = is_numeric($data['step_minutes'] ?? null)
            ? (int) $data['step_minutes']
            : AvailabilityEngine::DEFAULT_STEP_MINUTES;

        $slots = $this->engine->search(
            $plan,
            $address,
            $from,
            $to,
            $step,
            null,
            $this->strategyFor($data['doctor_uuid'] ?? null),
            $this->preferredResourceIds($user, $data['course_uuid'] ?? null),
        );

        return $this->success([
            'plan'  => $plan->toArray(),
            'slots' => array_map(static fn (AvailableSlot $s): array => $s->toArray(), $slots),
            // فهرست خالی خطا نیست: ممکن است واقعاً ظرفیتی نباشد. دلیلش صریح می‌آید
            // تا کلاینت مجبور نباشد از خالی بودن حدس بزند.
            'reason' => $slots === [] ? 'no_capacity_in_range' : null,
        ]);
    }

    /**
     * نمای ماه: فقط «این روز ظرفیت دارد یا نه». عمداً سبک است — تقویم ماهانه نباید
     * تخصیص منبع هر زمان را بسازد.
     */
    #[Route('/api/v1/appointment-availability/month', name: 'appointment_availability_month', methods: ['GET'])]
    public function month(#[CurrentUser] User $user, Request $request): JsonResponse
    {
        $serviceUuid = $request->query->get('service_uuid');
        $branchUuid  = $request->query->get('branch_uuid');
        $from        = $request->query->get('from');
        $to          = $request->query->get('to');

        if (!is_string($serviceUuid) || !is_string($branchUuid) || !is_numeric($from) || !is_numeric($to)) {
            return $this->error(ErrorCodes::ERR_VALIDATION_002, 'service_uuid، branch_uuid، from و to الزامی‌اند', 422, 'service_uuid');
        }

        $address = $this->branches->resolve($user, $branchUuid);
        $service = $this->requireItem($user, $serviceUuid);

        $this->assertResourceMode($request->query->get('doctor_uuid'), $address);

        $plan  = $this->planner->build($service, [], $address);
        $slots = $this->engine->search($plan, $address, (int) $from, (int) $to);

        $days = [];
        foreach ($slots as $slot) {
            $midnight = (new \DateTimeImmutable('@' . $slot->start))
                ->setTimezone(new \DateTimeZone($address->getTimezone()))
                ->setTime(0, 0)
                ->getTimestamp();

            $days[$midnight] = true;
        }

        return $this->success(['days' => array_keys($days)]);
    }

    /**
     * فقط محلی که صریحاً روی حالت منبع رفته این مسیر را دارد.
     *
     * @throws AppException
     */
    /**
     * استراتژی ترتیب منابع از تنظیمات همان محل می‌آید.
     *
     * کلید ناشناخته یا نبودِ برنامهٔ هفتگی به پیش‌فرض برمی‌گردد — تنظیماتِ ناقص نباید
     * جستجوی وقت را بخواباند.
     */
    private function strategyFor(mixed $doctorUuid): ?string
    {
        if (!is_string($doctorUuid) || $doctorUuid === '') {
            return null;
        }

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

        if ($doctor === null) {
            return null;
        }

        foreach ($this->schedules->findAllByDoctor($doctor) as $schedule) {
            $meta = $schedule->getMeta();

            if (($meta['booking_mode'] ?? null) === WeeklySchedule::MODE_RESOURCE) {
                return is_string($meta['resource_strategy'] ?? null) ? $meta['resource_strategy'] : null;
            }
        }

        return null;
    }

    /**
     * منبع ترجیحیِ یک دورهٔ درمان — «همان اپراتور جلسهٔ قبل».
     *
     * ترجیح است نه فیلتر: اگر آزاد نباشد، استراتژی به ترتیب پایه برمی‌گردد و رزرو
     * انجام می‌شود. اجبار یعنی بیمار دو هفته منتظر بماند.
     *
     * @return list<int>
     */
    private function preferredResourceIds(User $user, mixed $courseUuid): array
    {
        if (!is_string($courseUuid) || $courseUuid === '') {
            return [];
        }

        $course = $this->courses->findByUuid($courseUuid);

        if ($course === null) {
            return [];
        }

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

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

        $preferred = $course->getPreferredResource();

        return $preferred === null ? [] : [(int) $preferred->getId()];
    }

    private function assertResourceMode(mixed $doctorUuid, DoctorAddress $address): void
    {
        if (!is_string($doctorUuid) || $doctorUuid === '') {
            return;   // بدون پزشک، حالت از برنامهٔ هفتگی قابل استنتاج نیست
        }

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

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

        foreach ($this->schedules->findAllByDoctor($doctor) as $schedule) {
            if (($schedule->getMeta()['booking_mode'] ?? null) === WeeklySchedule::MODE_RESOURCE) {
                return;
            }
        }

        throw new AppException(
            ErrorCodes::ERR_WRONG_BOOKING_MODE,
            'این محل هنوز روی نوبت‌دهی چندمنبعی نیست',
            422,
            'doctor_uuid',
        );
    }

    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;
    }
}
