<?php

namespace App\Course\Service;

use App\Appointment\Availability\Service\AvailabilityEngine;
use App\Appointment\Availability\ValueObject\AvailableSlot;
use App\Appointment\Plan\Service\AppointmentPlanBuilder;
use App\Course\Entity\CourseSession;
use App\Course\Entity\TreatmentCourse;
use App\Doctor\Entity\DoctorAddress;
use App\Policy\Entity\Policy;
use App\Policy\Engine\SpacingPolicyEngine;
use App\Policy\Service\PolicySchema;

/**
 * برنامه‌ریزی جلسات دوره: پیشنهاد جلسهٔ بعدی و رزرو یکجا.
 *
 * ## لنگر متحرک
 *
 * فاصله همیشه از **جلسهٔ قبلی** حساب می‌شود، نه از شروع دوره. اگر جلسهٔ ۲ سه روز دیرتر
 * افتاد، جلسهٔ ۳ هم جابه‌جا می‌شود — وگرنه تأخیر یک جلسه، فاصلهٔ بقیه را خراب می‌کند.
 *
 * ## نزدیک‌ترین به ایده‌آل، نه اولین آزاد
 *
 * ۲۸ روز ایده‌آل است؛ روز ۲۱ (حداقلِ مجاز) از نظر درمانی بدتر از روز ۲۷ است. پس بین
 * وقت‌های موجود، آن که فاصله‌اش تا ایده‌آل کمتر است برنده می‌شود.
 */
final class CourseScheduler
{
    /** سقف جستجوی تسک ۰۶ — جلسات بیرون این بازه `planned` می‌مانند. */
    public const SEARCH_HORIZON_DAYS = 90;

    public function __construct(
        private readonly AppointmentPlanBuilder $planner,
        private readonly AvailabilityEngine     $availability,
        private readonly SpacingPolicyEngine         $spacingPolicies,
    ) {}

    /**
     * فاصلهٔ مؤثر: سخت‌گیرانه‌ترین بین پروتکل دوره و قانون `spacing` تسک ۰۹.
     *
     * قانون کلینیک نباید با پروتکل بجنگد؛ هر کدام سخت‌گیرتر بود همان اجرا می‌شود.
     */
    public function effectiveMinDays(TreatmentCourse $course, ?int $at = null): int
    {
        $service = $course->getServiceItem();

        $outcome = $this->spacingPolicies->evaluate(
            $course->getEntityType(),
            $course->getEntityId(),
            [
                'service_uuid'     => $service->getUuid(),
                'catalog_category' => $service->getCatalogCategory()?->getUuid(),
            ],
            null,
            $service,
            $at,
        );

        return max($course->getMinDays(), (int) $outcome->effect(PolicySchema::EFFECT_MIN_DAYS_BETWEEN, 0));
    }

    /**
     * پیشنهاد برای جلسهٔ بعدی: بازهٔ مجاز، تاریخ ایده‌آل، چند وقت نزدیک به آن، و
     * هشدار عبور از حداکثر فاصله.
     *
     * @return array<string, mixed>
     */
    public function suggestNext(TreatmentCourse $course, DoctorAddress $address, ?int $now = null): array
    {
        $now     = $now ?? time();
        $session = $this->nextPlanned($course);

        if ($session === null) {
            return ['session_number' => null, 'suggested_slots' => [], 'warning' => 'همهٔ جلسات این دوره برنامه‌ریزی شده‌اند'];
        }

        $anchor = $course->lastCompletedAt() ?? $course->getStartedAt();
        $minDays = $this->effectiveMinDays($course, $now);

        $min   = $anchor + $minDays * 86400;
        $ideal = $anchor + max($course->getIdealDays(), $minDays) * 86400;
        $max   = $anchor + max($course->getMaxDays(), $minDays) * 86400;

        // زمان گذشته پیشنهاد نمی‌شود؛ بیمارِ دیرکرده باید از همین حالا وقت بگیرد.
        $searchFrom = max($min, $now);
        $searchTo   = max($max, $searchFrom + 86400);

        $slots = $this->slotsFor($course, $address, $searchFrom, $searchTo, $ideal, $now);

        return [
            'session_number'  => $session->getSessionNumber(),
            'params'          => (object) $session->getParams(),
            'ideal_at'        => $ideal,
            'range'           => ['min' => $min, 'max' => $max],
            'suggested_slots' => array_map(
                static fn (AvailableSlot $s): array => ['start' => $s->start, 'end' => $s->end],
                array_slice($slots, 0, 3),
            ),
            // هشدار وقتی معنا دارد که واقعاً دیر شده باشد، نه وقتی هنوز فرصت هست.
            'warning' => $now > $max
                ? sprintf('از حداکثر فاصلهٔ مجاز (%d روز) عبور شده است. برای ادامهٔ دوره با پزشک مشورت کنید.', $course->getMaxDays())
                : null,
        ];
    }

    /**
     * نزدیک‌ترین وقت به ایده‌آل، داخل بازهٔ مجاز.
     *
     * @return AvailableSlot[] مرتب بر اساس فاصله تا ایده‌آل
     */
    public function slotsFor(
        TreatmentCourse $course,
        DoctorAddress $address,
        int $from,
        int $to,
        int $ideal,
        ?int $now = null,
    ): array {
        $plan  = $this->planner->build($course->getServiceItem(), [], $address);
        $slots = $this->availability->search($plan, $address, $from, $to, now: $now);

        usort($slots, static fn (AvailableSlot $a, AvailableSlot $b): int
            => abs($a->start - $ideal) <=> abs($b->start - $ideal));

        return $slots;
    }

    public function nextPlanned(TreatmentCourse $course): ?CourseSession
    {
        $planned = $course->plannedSessions();

        usort($planned, static fn (CourseSession $a, CourseSession $b): int
            => $a->getSessionNumber() <=> $b->getSessionNumber());

        return $planned[0] ?? null;
    }
}
