<?php

namespace App\Shared\Context;

use App\Auth\Entity\User;
use App\Auth\Repository\UserActiveContextRepository;
use App\Clinic\Entity\Clinic;
use App\Clinic\Repository\ClinicRepository;
use App\Doctor\Entity\Doctor;
use App\Doctor\Repository\DoctorRepository;
use App\Secretary\Repository\DoctorSecretaryRepository;
use App\Shared\Constant\ErrorCodes;
use App\Shared\Exception\AppException;
use App\Staff\Repository\ClinicStaffRepository;

/**
 * تنها نقطهٔ تصمیم‌گیری دربارهٔ «این درخواست در کدام محیط اجرا می‌شود؟».
 *
 * اولویت: clinic_uuid صریحِ درخواست > محیط فعالِ ذخیره‌شدهٔ کاربر > نقش کاربر.
 *
 * نقش به‌تنهایی برای کاربری که هم پزشک است و هم مالک کلینیک جواب نمی‌دهد: چنین
 * کاربری همیشه به‌عنوان پزشک حل می‌شد و هرگز به سرویس‌های کلینیک خودش نمی‌رسید.
 * UserActiveContext تعیین‌کننده است و نقش فقط fallback آخر.
 *
 * این کلاس فقط به «کدام محیط» جواب می‌دهد. «چه کاری در آن محیط مجاز است» کار
 * ClinicDoctorPermissionChecker و SecretaryPermissionChecker است و اینجا بررسی نمی‌شود.
 *
 * برای مسیر رزرو عمومی (که کاربر پنل ندارد و ورودی‌اش Doctor است، نه User)
 * قرینه‌ای جدا وجود دارد: {@see \App\Appointment\Service\BookingContextResolver}.
 */
class EntityContextResolver
{
    public function __construct(
        private readonly DoctorRepository            $doctorRepo,
        private readonly ClinicRepository            $clinicRepo,
        private readonly UserActiveContextRepository $activeContextRepo,
        private readonly DoctorSecretaryRepository   $secretaryRepo,
        private readonly ClinicStaffRepository       $staffRepo,
    ) {}

    /**
     * @param string|null $clinicUuid اگر داده شود، محیط کلینیک اجباری می‌شود و در
     *                                صورت نداشتن دسترسی، خطای ۴۰۳ پرتاب می‌شود.
     */
    public function resolve(User $user, ?string $clinicUuid = null): EntityContext
    {
        if ($clinicUuid !== null && $clinicUuid !== '') {
            $clinic = $this->clinicRepo->findByUuid($clinicUuid);
            if ($clinic === null) {
                throw new AppException(ErrorCodes::ERR_VALIDATION_002, 'کلینیک یافت نشد', 404);
            }
            $this->assertCanActInClinic($user, $clinic);

            return EntityContext::forClinic($clinic)->asChosen();
        }

        $fromActive = $this->fromActiveContext($user);
        if ($fromActive !== null) {
            return $fromActive;
        }

        return $this->fromRole($user);
    }

    /**
     * محیط را بدون پرتاب خطا حل می‌کند؛ اگر کاربر به کلینیکِ خواسته‌شده دسترسی
     * نداشته باشد null برمی‌گرداند. برای مسیرهایی که خودشان authorization جدا دارند.
     */
    public function tryResolve(User $user, ?string $clinicUuid = null): ?EntityContext
    {
        try {
            return $this->resolve($user, $clinicUuid);
        } catch (AppException) {
            return null;
        }
    }

    /**
     * محیطی که کاربر **صاحبش** است، مستقل از محیط فعال و از نقش‌هایش.
     *
     * برای خریدهایی است که به حساب خودِ صاحب می‌نشیند (اشتراک): آنجا «کجا ایستاده‌ام»
     * مهم نیست، «چه چیزی دارم» مهم است. تنها مرجعِ این پرسش همین متد است تا پرداختِ
     * اشتراک و خودِ اشتراک هرگز روی دو محیط متفاوت ننشینند.
     */
    public function ownedEntity(User $user): EntityContext
    {
        $doctor = $this->doctorRepo->findByUser($user);
        if ($doctor !== null) {
            return EntityContext::forDoctor($doctor);
        }

        $clinic = $this->clinicRepo->findByUser($user);

        return $clinic !== null ? EntityContext::forClinic($clinic) : EntityContext::unknown();
    }

    /**
     * مالک کلینیک، ادمین، پزشکِ عضو همان کلینیک، منشیِ دارای رابطهٔ فعال در آن، یا
     * پرسنلِ فعالِ همان کلینیک.
     *
     * «می‌تواند در این محیط بایستد» یعنی محیطش حل می‌شود — نه اینکه هر کاری در آن
     * مجاز است؛ محدودهٔ پرسنل را StaffRouteGuardSubscriber تعیین می‌کند.
     */
    public function canActInClinic(User $user, Clinic $clinic): bool
    {
        if ($user->hasRole('ROLE_ADMIN') || $clinic->getUser()->getId() === $user->getId()) {
            return true;
        }

        $doctor = $this->doctorRepo->findByUser($user);
        if ($doctor !== null && $clinic->hasDoctor($doctor)) {
            return true;
        }

        if ($this->staffRepo->findActiveByUserAndEntity($user, EntityContext::TYPE_CLINIC, $clinic->getId()) !== null) {
            return true;
        }

        return $this->secretaryRepo->findActiveBySecretaryForClinic($user, $clinic) !== null;
    }

    public function assertCanActInClinic(User $user, Clinic $clinic): void
    {
        if (!$this->canActInClinic($user, $clinic)) {
            throw new AppException(ErrorCodes::ERR_ACCESS_DENIED, 'به این کلینیک دسترسی ندارید', 403);
        }
    }

    /** محیط فعالِ ذخیره‌شده؛ db_type می‌گوید db_uuid کدام موجودیت را آدرس می‌دهد. */
    private function fromActiveContext(User $user): ?EntityContext
    {
        $active = $this->activeContextRepo->findByUser($user);
        if ($active === null) {
            return null;
        }

        if ($active->isClinic()) {
            $clinic = $this->clinicRepo->findByUuid($active->getDbUuid());

            return $clinic !== null && $this->canActInClinic($user, $clinic)
                ? EntityContext::forClinic($clinic)->asChosen()
                : null;
        }

        $doctor = $this->doctorRepo->findByUuid($active->getDbUuid());

        return $doctor !== null && $this->canActForDoctor($user, $doctor)
            ? EntityContext::forDoctor($doctor)->asChosen()
            : null;
    }

    /**
     * محیطِ مطب شخصی: خودِ پزشک، یا منشیِ دارای رابطهٔ فعال با او.
     * قرینهٔ canActInClinic برای شاخهٔ غیرکلینیکی؛ ادمین عمداً اینجا نیست چون
     * مسیرهای ادمین سراسری‌اند و از محیط فعالِ یک پزشک عبور نمی‌کنند.
     */
    private function canActForDoctor(User $user, Doctor $doctor): bool
    {
        if ($doctor->getUser()->getId() === $user->getId()) {
            return true;
        }

        if ($this->staffRepo->findActiveByUserAndEntity($user, EntityContext::TYPE_DOCTOR, $doctor->getId()) !== null) {
            return true;
        }

        return $this->secretaryRepo->findActiveBySecretaryForDoctor($user, $doctor) !== null;
    }

    private function fromRole(User $user): EntityContext
    {
        if ($user->hasRole('ROLE_DOCTOR')) {
            return EntityContext::forDoctor($this->doctorRepo->findByUser($user));
        }

        if ($user->hasRole('ROLE_CLINIC')) {
            $clinic = $this->clinicRepo->findByUser($user);

            return $clinic !== null ? EntityContext::forClinic($clinic) : EntityContext::unknown();
        }

        // منشی و پرسنل fallback نقشی ندارند: محیطشان فقط از UserActiveContext می‌آید،
        // چون هر دو می‌توانند هم‌زمان به چند پزشک و کلینیک وصل باشند و نقش تنها،
        // انتخاب بین آن‌ها را تعیین نمی‌کند.
        return EntityContext::unknown();
    }
}
