<?php

namespace App\Appointment\Service;

use App\Appointment\Entity\WeeklySchedule;
use App\Appointment\Repository\WeeklyScheduleRepository;
use App\Appointment\ValueObject\ServiceBookingDuration;
use App\Clinic\Entity\Clinic;
use App\ClinicService\Repository\ServiceItemRepository;
use App\ClinicService\Service\DurationCalculator;
use App\Doctor\Entity\Doctor;
use App\Shared\Constant\ErrorCodes;
use App\Shared\Exception\AppException;
use App\Shared\Tenant\TenantOwnershipChecker;

/**
 * تنها مرجعِ «مدتِ مجازِ یک ترکیب سرویس» در حالت نوبت‌دهی سرویسی.
 *
 * این منطق پیش‌تر داخل `AppointmentController::serviceSlots()` بود و سه مصرف‌کنندهٔ تازه
 * (اعتبارسنجی PATCH، جابه‌جایی سرویس‌آگاه، تبدیل نوبت رزرو) به همان محاسبه نیاز داشتند.
 * کپی‌کردنش یعنی چهار نسخه با چهار رفتار مرزی متفاوت، پس یک‌جا شد.
 *
 * ⚠️ فرمول فعلی جمعِ سادهٔ مدت‌هاست. این عمداً حفظ شده: تغییرش به «زمان تنها / زمان اضافه»
 * (بند ۵ مستند موتور نوبت‌دهی) کار تسک ۰۴ است و همان‌جا **یک خط** از این کلاس عوض می‌شود.
 * اصلاحش در این تسک، مدت همهٔ نوبت‌های چندسرویسیِ در جریان را بی‌ورودی جدید تغییر می‌داد.
 */
final class ServiceBookingCalculator
{
    public function __construct(
        private readonly ServiceItemRepository    $itemRepo,
        private readonly DurationCalculator       $durations,
        private readonly WeeklyScheduleRepository $schedules,
        private readonly TenantOwnershipChecker   $ownership,
    ) {}

    /** آیا این محل نوبت‌دهی در حالت سرویسی است. تنها معیار، `booking_mode` است. */
    public function isServiceMode(Doctor $doctor, ?Clinic $clinic): bool
    {
        return ($this->metaOf($doctor, $clinic)['booking_mode'] ?? WeeklySchedule::MODE_SLOT)
            === WeeklySchedule::MODE_SERVICE;
    }

    public function bufferMinutes(Doctor $doctor, ?Clinic $clinic): int
    {
        return max(0, (int) ($this->metaOf($doctor, $clinic)['buffer_minutes'] ?? 0));
    }

    /**
     * مدت و بافرِ یک ترکیب سرویس.
     *
     * ترتیب بررسی عمدی است: **مالکیت محیط اول**. پیام‌های بعدی وجود و فعال‌بودن و مدت
     * سرویس را لو می‌دهند و همان نشتی‌ای می‌سازند که در فاز ۸ tenancy روی این endpoint
     * پیدا و رفع شد ({@see docs/architecture/tenancy.md}، جدول «uuid از درخواست»).
     *
     * @param string[]           $serviceUuids
     * @param array<string, int|string> $durationOverrides  uuid → دقیقه؛ override منشی، فقط
     *                                                      برای همین محاسبه (پیش‌فرض سرویس
     *                                                      دست‌نخورده می‌ماند)
     * @param bool $allowInactive سرویسِ غیرفعالِ **نوبتِ موجود** را رد نکن؛ به‌جایش warning بده.
     *                            نوبتی که کلینیک سرویسش را غیرفعال کرده باید قابل جابه‌جایی
     *                            و لغو بماند، وگرنه برای همیشه قفل می‌شود.
     *
     * @throws AppException همان کد/پیام/فیلدی که پیش‌تر کنترلر برمی‌گرداند
     */
    public function calculate(
        Doctor $doctor,
        ?Clinic $clinic,
        array $serviceUuids,
        array $durationOverrides = [],
        bool $allowInactive = false,
    ): ServiceBookingDuration {
        $this->assertBelongsToContext($serviceUuids, $doctor, $clinic);

        $resolved  = [];
        $warnings  = [];
        $overridden = [];

        foreach ($serviceUuids as $uuid) {
            $item = $this->itemRepo->findByUuid($uuid);
            if ($item === null) {
                throw new AppException(ErrorCodes::ERR_VALIDATION_002, 'سرویس یافت نشد', 422, 'service_item_uuids');
            }

            if (!$item->isBookable()) {
                if (!$allowInactive) {
                    throw new AppException(ErrorCodes::ERR_VALIDATION_001, 'این سرویس برای نوبت‌دهی فعال نیست', 422, 'service_item_uuids');
                }
                $warnings[] = sprintf('سرویس «%s» دیگر برای نوبت‌دهی فعال نیست', $item->getName());
            }

            $override = isset($durationOverrides[$uuid]) ? (int) $durationOverrides[$uuid] : 0;
            $duration = $override > 0 ? $override : (int) ($item->getSoloDurationMinutes() ?? 0);
            if ($duration <= 0) {
                throw new AppException(ErrorCodes::ERR_VALIDATION_001, 'مدت سرویس تعریف نشده است', 422, 'service_item_uuids');
            }

            if ($override > 0) {
                $overridden[(int) $item->getId()] = $override;
            }

            $resolved[] = $item;
        }

        return new ServiceBookingDuration(
            totalMinutes: $this->durations->totalMinutes($resolved, [], $overridden),
            bufferMinutes: $this->bufferMinutes($doctor, $clinic),
            serviceItems: $resolved,
            warnings: $warnings,
        );
    }

    /**
     * سرویس باید مالِ همین محل نوبت‌دهی باشد. uuid ناموجود هم همین خطا را می‌گیرد —
     * عمدی، تا نبودن سرویس از «سرویسِ محیط دیگر» قابل تفکیک نباشد.
     *
     * محیط از جفتِ **بوکینگ** ساخته می‌شود (کلینیک اگر باشد، وگرنه مطب شخصی پزشک)، نه از
     * محیط جاری کاربر: این مسیر عمومی است و ممکن است هیچ کاربر احراز‌شده‌ای نداشته باشد.
     * پس `belongsToPair()` استفاده می‌شود، نه `belongsTo()`.
     *
     * @param string[] $serviceUuids
     */
    private function assertBelongsToContext(array $serviceUuids, Doctor $doctor, ?Clinic $clinic): void
    {
        [$type, $id] = $clinic !== null
            ? ['clinic', $clinic->getId()]
            : ['doctor', $doctor->getId()];

        foreach ($serviceUuids as $uuid) {
            if (!$this->ownership->belongsToPair($type, $id, $this->itemRepo->findByUuid($uuid))) {
                throw new AppException(
                    ErrorCodes::ERR_VALIDATION_001,
                    'سرویس انتخاب‌شده به این محل نوبت‌دهی تعلق ندارد',
                    422,
                    'service_item_uuids',
                );
            }
        }
    }

    /** @return array<string, mixed> */
    private function metaOf(Doctor $doctor, ?Clinic $clinic): array
    {
        $schedule = $this->schedules->findByDoctorAndClinic($doctor, $clinic);

        return $schedule ? $schedule->getMeta() : WeeklySchedule::DEFAULT_META;
    }
}
