<?php

namespace App\Secretary\Security;

use App\Auth\Entity\User;
use App\Secretary\Entity\DoctorSecretary;
use App\Secretary\Repository\DoctorSecretaryRepository;
use App\Shared\Constant\ErrorCodes;
use App\Shared\Context\EntityContextResolver;
use App\Shared\Exception\AppException;

/**
 * Single entry point that answers «آیا این منشی مجاز به resource/action هست؟».
 *
 * منبع حقیقت، ستون JSON `permission` روی ردیف فعالِ DoctorSecretary در محیطِ
 * فعالِ کاربر است. محیط از EntityContextResolver می‌آید — همان نقطه‌ای که همهٔ
 * نقش‌ها از آن می‌گذرند؛ اینجا فقط مجوزها بررسی می‌شوند، نه محیط.
 */
class SecretaryAccessChecker
{
    public function __construct(
        private readonly EntityContextResolver       $contextResolver,
        private readonly DoctorSecretaryRepository   $secretaryRepo,
        private readonly SecretaryPermissionChecker  $permissions,
    ) {}

    /** ردیف فعالِ منشی در محیط فعال؛ null اگر محیط تنظیم نشده یا رابطه‌ای نیست. */
    public function activeRelation(User $user): ?DoctorSecretary
    {
        $context = $this->contextResolver->resolve($user);
        if (!$context->isResolved()) {
            return null;
        }

        return $context->isClinic()
            ? $this->secretaryRepo->findActiveBySecretaryForClinic($user, $context->clinic)
            : $this->secretaryRepo->findActiveBySecretaryForDoctor($user, $context->doctor);
    }

    public function can(User $user, string $resource, string $action): bool
    {
        $relation = $this->activeRelation($user);

        return $relation !== null && $this->permissions->can($relation, $resource, $action);
    }

    /**
     * جفتِ [entityType, entityId] مالکِ محیطِ فعالِ منشی — کلینیک یا پزشک.
     * برای کنترلرهایی که دادهٔ tenant را با این جفت واکشی می‌کنند. مجوز جدا با
     * denyUnlessGranted بررسی می‌شود؛ این متد فقط owner را حل می‌کند.
     *
     * @return array{0: string, 1: int|null} ['clinic'|'doctor'|'unknown', id|null]
     */
    public function resolveOwnerEntity(User $user): array
    {
        return $this->contextResolver->resolve($user)->toEntityPair();
    }

    /**
     * آیا منشی در محیطِ فعالِ خود، روی این پزشکِ مشخص (و کلینیکِ همان نوبت/تنظیم)
     * مجاز به resource/action است؟ ترکیبِ «اسکوپِ پزشکِ تخصیص‌یافته» و «توگلِ مجوز».
     * قرینهٔ AppointmentAccessChecker::secretaryCan اما برای هر resource.
     */
    public function canForDoctor(
        User $user,
        \App\Doctor\Entity\Doctor $doctor,
        ?\App\Clinic\Entity\Clinic $clinic,
        string $resource,
        string $action
    ): 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->permissions->can($relation, $resource, $action);
        }

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

        return $relation !== null && $this->permissions->can($relation, $resource, $action);
    }

    /**
     * idهای پزشکانِ تخصیص‌یافته به این منشی در این کلینیک — برای محدودکردنِ
     * لیست‌هایی که پیش‌فرض همهٔ پزشکانِ کلینیک را برمی‌گردانند. اگر منشی نیست یا
     * محیطش این کلینیک نیست → آرایهٔ خالی.
     *
     * @return int[]
     */
    public function assignedClinicDoctorIds(User $user, \App\Clinic\Entity\Clinic $clinic): array
    {
        if (!$user->hasRole('ROLE_SECRETARY')) {
            return [];
        }

        return array_map(
            static fn(\App\Doctor\Entity\Doctor $d) => $d->getId(),
            $this->secretaryRepo->findDoctorsBySecretaryInClinic($user, $clinic),
        );
    }

    /**
     * آیا منشی در محیطِ فعالِ خود — که باید همین کلینیک باشد — مجاز به resource/action است؟
     * برای منابعِ کلینیک‌سطح مثل clinic_doctors که tenant لزوماً کلینیک است.
     */
    public function canForClinic(User $user, \App\Clinic\Entity\Clinic $clinic, string $resource, string $action): bool
    {
        [$type, $id] = $this->resolveOwnerEntity($user);

        return $type === 'clinic' && $id === $clinic->getId() && $this->can($user, $resource, $action);
    }

    /**
     * برای مسیرهایی که چند نقش دارند: فقط منشی را محدود کن. سایر نقش‌ها true.
     */
    public function canOrNonSecretary(User $user, string $resource, string $action): bool
    {
        if (!$user->hasRole('ROLE_SECRETARY')) {
            return true;
        }

        return $this->can($user, $resource, $action);
    }

    /** 403 اگر منشی مجاز نباشد؛ نقش‌های دیگر بدون تغییر عبور می‌کنند. */
    public function denyUnlessGranted(User $user, string $resource, string $action): void
    {
        if (!$this->canOrNonSecretary($user, $resource, $action)) {
            throw new AppException(ErrorCodes::ERR_FORBIDDEN_001, null, 403);
        }
    }
}
