<?php

namespace App\Appointment\Security;

use App\Appointment\Entity\Appointment;
use App\Auth\Entity\User;
use App\Clinic\Repository\ClinicRepository;
use App\Clinic\Security\ClinicDoctorPermissionChecker;
use App\Secretary\Repository\DoctorSecretaryRepository;
use App\Secretary\Security\SecretaryPermissionChecker;
use App\Shared\Context\EntityContextResolver;

/**
 * تنها تصمیم‌گیرندهٔ دسترسی روی «یک نوبت مشخص».
 *
 * پیش از این، مسیرهای تک‌نوبت فقط بیمار، پزشکِ مالک و ادمین را می‌شناختند؛ نوبتی که
 * کاربر کلینیک از مسیر /my/appointment می‌ساخت، روی مشاهده و ویرایش ۴۰۳ می‌گرفت.
 * محیط نوبت با appointment.clinic بیان می‌شود (NULL یعنی مطب شخصی) و همان مبنای
 * تصمیم است — نه نقش کاربر.
 *
 * اکشن‌ها از همان واژگان ClinicDoctorPermission/DoctorSecretary گرفته شده‌اند تا
 * «پایان همکاری» فقط یک منبع حقیقت داشته باشد: active=false در همان رکوردها.
 */
class AppointmentAccessChecker
{
    public const ACTION_VIEW          = 'view';
    public const ACTION_UPDATE_STATUS = 'update_status';
    public const ACTION_CANCEL        = 'cancel';

    private const RESOURCE = 'appointments';

    public function __construct(
        private readonly ClinicDoctorPermissionChecker $clinicPermissions,
        private readonly SecretaryPermissionChecker    $secretaryPermissions,
        private readonly DoctorSecretaryRepository     $secretaryRepo,
        private readonly EntityContextResolver         $contextResolver,
        private readonly ClinicRepository              $clinicRepo,
    ) {}

    public function canView(Appointment $appointment, User $user): bool
    {
        return $this->can($appointment, $user, self::ACTION_VIEW);
    }

    /** مجوز تغییر نوبت: ویرایش، جابه‌جایی، رزرو، جایگزینی و تغییر وضعیت. */
    public function canManage(Appointment $appointment, User $user): bool
    {
        return $this->can($appointment, $user, self::ACTION_UPDATE_STATUS);
    }

    public function canCancel(Appointment $appointment, User $user): bool
    {
        return $this->can($appointment, $user, self::ACTION_CANCEL);
    }

    public function can(Appointment $appointment, User $user, string $action): bool
    {
        if ($user->hasRole('ROLE_ADMIN')) {
            return true;
        }

        if ($appointment->getDoctor()->getUser()->getId() === $user->getId()) {
            return true;
        }

        // بیمار نوبت خودش را می‌بیند و لغو می‌کند، ولی جابه‌جا/ویرایش نمی‌کند.
        if ($appointment->getUser()->getId() === $user->getId()) {
            return $action === self::ACTION_VIEW || $action === self::ACTION_CANCEL;
        }

        $clinic = $appointment->getClinic();
        if ($clinic !== null && $this->clinicPermissions->can($user, $clinic, self::RESOURCE, $action)) {
            return true;
        }

        return $this->secretaryCan($appointment, $user, $action);
    }

    /**
     * آیا این کاربر می‌تواند در محیطِ (پزشک + کلینیک) نوبت مدیریت/ثبت کند — بدون آنکه
     * هنوز نوبتی وجود داشته باشد. برای اندپوینت‌های اسلات که عمومی‌اند ولی وقتی از پنل
     * (با management=1) صدا زده می‌شوند باید توگلِ نوبت‌دهی آنلاین را دور بزنند.
     *
     * منطق همان can() است اما روی محیط، نه روی یک Appointment مشخص.
     */
    public function canManageContext(User $user, \App\Doctor\Entity\Doctor $doctor, ?\App\Clinic\Entity\Clinic $clinic): bool
    {
        if ($user->hasRole('ROLE_ADMIN')) {
            return true;
        }

        if ($doctor->getUser()->getId() === $user->getId()) {
            return true;
        }

        if ($clinic !== null && $this->clinicPermissions->can($user, $clinic, self::RESOURCE, self::ACTION_UPDATE_STATUS)) {
            return true;
        }

        return $this->secretaryCanContext($user, $doctor, $clinic);
    }

    /**
     * منشی در محیطِ فعالِ خودش، اما روی محیط (پزشک/کلینیک) نه یک نوبت مشخص.
     * قرینهٔ secretaryCan() است.
     */
    private function secretaryCanContext(User $user, \App\Doctor\Entity\Doctor $doctor, ?\App\Clinic\Entity\Clinic $clinic): bool
    {
        $context = $this->contextResolver->resolve($user);
        if (!$context->isResolved()) {
            return false;
        }

        if ($context->isClinic()) {
            if ($clinic === null || $context->id !== $clinic->getId()) {
                return false;
            }

            $relation = $this->secretaryRepo->findActiveClinicRow($user, $context->clinic, $doctor);

            return $relation !== null && $this->secretaryPermissions->can($relation, self::RESOURCE, self::ACTION_UPDATE_STATUS);
        }

        // محیطِ مطب شخصی: نوبت هم باید در همان مطب شخصی باشد (clinic == null).
        if ($clinic !== null || $context->id !== $doctor->getId()) {
            return false;
        }

        $relation = $this->secretaryRepo->findActiveBySecretaryForDoctor($user, $doctor);

        return $relation !== null && $this->secretaryPermissions->can($relation, self::RESOURCE, self::ACTION_UPDATE_STATUS);
    }

    /**
     * کلینیکی که این کاربر در آن اجازهٔ دیدن نوبت‌های این پزشک را دارد، یا null.
     * برای لیست‌هایی که باید به یک محیط محدود شوند (نه تک‌نوبت).
     */
    public function viewableClinicFor(User $user, \App\Doctor\Entity\Doctor $doctor): ?\App\Clinic\Entity\Clinic
    {
        $context = $this->contextResolver->resolve($user);

        // بیرون از محیط کلینیک، کلینیکِ تحتِ مالکیت بازمی‌گردد: کاربری که هم پزشک
        // است و هم مالک کلینیک، بدون محیط فعال به‌عنوان پزشک حل می‌شود ولی هنوز
        // باید لیست نوبت‌های کلینیک خودش را ببیند.
        $clinic = $context->isClinic() ? $context->clinic : $this->clinicRepo->findByUser($user);

        if ($clinic === null || !$clinic->hasDoctor($doctor)) {
            return null;
        }

        return $this->clinicPermissions->can($user, $clinic, self::RESOURCE, self::ACTION_VIEW)
            ? $clinic
            : null;
    }

    /**
     * منشی در محیط فعالِ خودش. در محیط کلینیک، نوبت باید هم متعلق به همان کلینیک
     * باشد و هم پزشکش جزو پزشکان تخصیص‌یافته به این منشی — عضویت در کلینیک به‌تنهایی
     * یعنی منشیِ یک پزشک بتواند نوبت پزشک دیگری را دست‌کاری کند.
     */
    private function secretaryCan(Appointment $appointment, User $user, string $action): bool
    {
        $context = $this->contextResolver->resolve($user);
        if (!$context->isResolved()) {
            return false;
        }

        if ($context->isClinic()) {
            if ($appointment->getClinic()?->getId() !== $context->id) {
                return false;
            }

            $relation = $this->secretaryRepo->findActiveClinicRow($user, $context->clinic, $appointment->getDoctor());

            return $relation !== null && $this->secretaryPermissions->can($relation, self::RESOURCE, $action);
        }

        $doctor = $appointment->getDoctor();
        if ($context->id !== $doctor->getId()) {
            return false;
        }

        $relation = $this->secretaryRepo->findActiveBySecretaryForDoctor($user, $doctor);

        return $relation !== null && $this->secretaryPermissions->can($relation, self::RESOURCE, $action);
    }
}
