<?php

namespace App\Shared\Tenant;

/**
 * طبقه‌بندی هر entity نسبت به جداسازی محیط. هر کلاس باید دقیقاً در یکی از این
 * چهار وضعیت باشد، وگرنه TenantSchemaCoverageTest قرمز می‌شود:
 *
 *   ۱. خودش جفت (entity_type, entity_id) دارد    → TenantFilter پوششش می‌دهد
 *   ۲. {@see self::ENTITIES}                     → عمداً سراسری است
 *   ۳. {@see self::AGGREGATE_CHILDREN}           → محیط را از ریشه به ارث می‌برد
 *   ۴. {@see self::DEFERRED}                     → هنوز طبقه‌بندی نشده، بدهی ثبت‌شده
 *
 * این فهرست تنها راه فرار از پوشش tenant است؛ افزودن به آن باید دلیل داشته باشد.
 */
final class GlobalTables
{
    /**
     * Entityهایی که به هیچ محیطی تعلق ندارند.
     *
     * @var array<class-string, string> کلاس => دلیل
     */
    public const ENTITIES = [
        // دادهٔ مرجع مشترک بین همهٔ محیط‌ها
        \App\Location\Entity\Province::class            => 'تقسیمات کشوری',
        \App\Location\Entity\City::class                => 'تقسیمات کشوری',
        \App\Specialty\Entity\Specialty::class          => 'تاکسونومی سراسری تخصص‌ها',
        \App\DoctorService\Entity\DoctorService::class  => 'تاکسونومی سراسری خدمات، وابسته به تخصص نه به محیط',
        \App\Insurance\Entity\Insurance::class          => 'فهرست بیمه‌های کشور',
        \App\Insurance\Entity\InsuranceCoverageDefault::class => 'پیش‌فرض پوشش بیمه در سطح کشور؛ هر محیط با TenantInsurance بازنویسی‌اش می‌کند',
        \App\Tag\Entity\Tag::class                      => 'تاکسونومی سراسری برچسب — قرینهٔ per-tenant آن TenantTag است',
        \App\Config\Entity\SiteConfig::class            => 'تنظیمات کل سامانه',
        \App\Config\Entity\TaxRateHistory::class        => 'نرخ مالیات کشور',
        \App\Subscription\Entity\SubscriptionPlan::class   => 'پلن‌های فروش، مشترک بین همهٔ مشتریان',
        \App\Subscription\Entity\SubscriptionPeriod::class => 'دوره‌های قیمتی همان پلن‌ها',
        \App\Sms\Entity\SmsTemplate::class              => 'قالب پیامک سامانه',
        \App\Sms\Entity\SmsMessageTemplate::class       => 'متن پیامک سامانه',
        \App\Sms\Entity\SmsLog::class                   => 'لاگ ارسال؛ فقط شماره و قالب دارد، مالک ندارد',
        \App\Shared\Logging\AppLog::class               => 'لاگ سراسری برنامه',
        \App\Blog\Entity\Blog::class                    => 'محتوای عمومی مارکت‌پلیس',
        \App\Resource\Entity\NationalHoliday::class     => 'تعطیلات رسمی کشور؛ per محیط کردنش معنای «کشوری» را از بین می‌برد — محیطی که خلافش کار می‌کند TenantHolidayOverride می‌زند',

        // هویت — یک شخص می‌تواند در چند محیط حضور داشته باشد
        \App\Auth\Entity\User::class            => 'هویت سراسری؛ رابطهٔ بیمار با محیط از patient_records می‌آید',
        \App\UserProfile\Entity\UserProfile::class => 'پروفایل شخص، نه دادهٔ محیط',
        \App\Auth\Entity\PreRegistration::class  => 'پیش‌ثبت‌نام، هنوز به هیچ محیطی وصل نیست',
        \App\Auth\Entity\UserActiveContext::class => 'خودش تعیین‌کنندهٔ محیط است؛ فیلتر کردنش حلقه می‌سازد',

        // خودِ محیط‌ها
        \App\Doctor\Entity\Doctor::class => 'خودش یک محیط است',
        \App\Clinic\Entity\Clinic::class => 'خودش یک محیط است',

        // دادهٔ عمومی مارکت‌پلیس دربارهٔ پزشک — بیمار می‌نویسد، نه محیط
        \App\Rating\Entity\Comment::class => 'نظر عمومی بیمار روی پروفایل پزشک',
        \App\Rating\Entity\Like::class    => 'لایک عمومی روی همان نظرها',
        \App\Rating\Entity\Rate::class    => 'امتیاز عمومی بیمار به پزشک',
        \App\Representation\Entity\Representation::class => 'نمایندهٔ فروش؛ بالادستِ محیط‌هاست نه داخل یکی',

        // رابطهٔ بین دو محیط — فیلتر کردن با یک طرف، طرف دیگر را کور می‌کند
        \App\Clinic\Entity\ClinicDoctorPermission::class => 'مجوز پزشکِ عضو در یک کلینیک؛ هویتش خودِ جفت (کلینیک، پزشک) است',
        \App\ClinicInvitation\Entity\ClinicDoctorInvitation::class => 'دعوت کلینیک از پزشک؛ پیش از عضویت هر دو طرف باید ببینندش',
        \App\Doctor\Entity\DoctorClaimRequest::class => 'درخواست تصاحب پروفایل پزشک؛ متقاضی هنوز صاحب محیط نیست',

        // دادهٔ خودِ پزشک، مستقل از اینکه در کدام کلینیک کار می‌کند
        \App\Doctor\Entity\DoctorAddress::class   => 'آدرس‌های پزشک؛ در همهٔ محیط‌های او یکسان است',
        \App\Insurance\Entity\DoctorInsurance::class => 'بیمه‌های طرف قرارداد خودِ پزشک',

        // استثنای مستندشده در فاز ۲
        \App\Appointment\Entity\Holiday::class => 'clinic=NULL یعنی «همهٔ محیط‌ها»، نه «مطب شخصی» — جفت tenant این را نمی‌تواند بیان کند',

        // کیف پولِ شخص — استثنای مستندشده در فاز ۶
        \App\Settlement\Entity\WalletTransaction::class => 'کیف پول خودِ شخص است نه محیط: موجودی از مجموع credit−debitِ همان کاربر مشتق می‌شود و payment_id تهی‌پذیر است، پس تفکیک به محیط، موجودی را بی‌معنا می‌کند',
        \App\Settlement\Entity\Settlement::class        => 'برداشت از همان کیف پولِ شخصی (SettlementController موجودی را با getWalletBalance(user) می‌سنجد)؛ محیط ندارد چون کیف پول ندارد',
    ];

    /**
     * فرزندان aggregate: ستون tenant ندارند و محیط را از ریشه به ارث می‌برند.
     * ریشه صریح اعلام می‌شود چون بعضی‌شان با FK اسکالر وصل‌اند (نه رابطهٔ Doctrine)
     * و از metadata قابل استنتاج نیستند.
     *
     * ⚠️ TenantFilter روی این‌ها اعمال نمی‌شود. کوئری مستقیم روی این جدول‌ها بدون
     * JOIN به ریشه، cross-tenant است — همیشه از ریشه شروع کن.
     *
     * فرزندی که uuidش از خودِ درخواست می‌آید نباید اینجا بماند: چنین جست‌وجویی
     * ذاتاً بی‌لنگر است و تور ایمنی ندارد. هشت مورد از این دست جفت محیط خودشان را
     * گرفتند (فاز ۸)؛ باقی‌مانده‌ها فقط از ریشه پیمایش می‌شوند.
     *
     * @var array<class-string, class-string> فرزند => ریشه
     */
    public const AGGREGATE_CHILDREN = [
        \App\Appointment\Entity\AppointmentEvent::class => \App\Appointment\Entity\Appointment::class,
        \App\Appointment\Plan\Entity\SegmentRequirement::class => \App\Appointment\Plan\Entity\SegmentTemplate::class,
        \App\Appointment\Booking\Entity\AppointmentSegment::class => \App\Appointment\Entity\Appointment::class,
        // سطل‌ها فقط قیدِ یکتاییِ ردیف اشغال‌اند و هیچ‌وقت مستقیم پرس‌وجو نمی‌شوند.
        \App\Appointment\Booking\Entity\OccupancyBucket::class => \App\Appointment\Availability\Entity\ResourceOccupancy::class,

        // ریشه‌هاشان خودشان جفت محیط دارند (برخلاف پروندهٔ branch_working_hours در
        // تسک ۰۱)، پس ارث‌بری اینجا واقعی است. هیچ‌کدام uuid از request نمی‌گیرند:
        // تنها راهشان PUT روی /resource/{uuid}/skills و /resource-pool/{uuid}/members است.
        \App\Resource\Entity\ResourceSkill::class       => \App\Resource\Entity\ClinicResource::class,
        \App\Resource\Entity\ResourceCalendar::class    => \App\Resource\Entity\ClinicResource::class,
        \App\Resource\Entity\ResourcePoolMember::class  => \App\Resource\Entity\ResourcePool::class,

        \App\Patient\Entity\SessionAuditLog::class      => \App\Patient\Entity\PatientSession::class,
        \App\Patient\Entity\SessionConsumable::class    => \App\Patient\Entity\PatientSession::class,
        \App\Patient\Entity\SessionService::class       => \App\Patient\Entity\PatientSession::class,

        \App\ClinicService\Entity\ServiceItemAuditLog::class   => \App\ClinicService\Entity\ServiceItem::class,
        \App\ClinicService\Entity\ServiceItemConsumable::class => \App\ClinicService\Entity\ServiceItem::class,
        \App\ClinicService\Entity\Tariff::class                => \App\ClinicService\Entity\ServiceItem::class,
        \App\ClinicService\Entity\ItemGroupMember::class      => \App\ClinicService\Entity\ItemGroup::class,
        \App\Pricing\Entity\PriceListItem::class            => \App\Pricing\Entity\PriceList::class,
        \App\Policy\Entity\PolicyVersionLog::class          => \App\Policy\Entity\Policy::class,

        \App\Billing\Entity\ClaimItem::class      => \App\Billing\Entity\Claim::class,
        \App\Billing\Entity\ClaimStatusLog::class => \App\Billing\Entity\Claim::class,
        \App\Billing\Entity\InvoiceItem::class    => \App\Billing\Entity\Invoice::class,

        \App\Inventory\Entity\InventoryPackageItem::class => \App\Inventory\Entity\InventoryPackage::class,

        // سرویس‌های یک پکیج جزئی از تعریف همان پکیج‌اند، نه دادهٔ مستقل.
        \App\Package\Entity\PackageService::class => \App\Package\Entity\Package::class,

        // پارامترهای هر جلسه جزئی از تعریف همان پروتکل‌اند.
        \App\Course\Entity\CourseProtocolStep::class => \App\Course\Entity\CourseProtocol::class,

        \App\Insurance\Entity\TenantInsuranceCategoryCoverage::class => \App\Insurance\Entity\TenantInsurance::class,
        \App\Insurance\Entity\TenantServiceCoverage::class           => \App\Insurance\Entity\TenantInsurance::class,

        \App\Sms\Entity\SmsWalletTransaction::class => \App\Sms\Entity\SmsWallet::class,

        \App\Payment\Entity\PaymentLog::class            => \App\Payment\Entity\Payment::class,
        \App\Settlement\Entity\FinancialBreakdown::class => \App\Payment\Entity\Payment::class,
        \App\Secretary\Entity\SecretaryEarning::class    => \App\Settlement\Entity\FinancialBreakdown::class,
    ];

    /**
     * بدهیِ طبقه‌بندی: کلاسی که هنوز تصمیمی درباره‌اش گرفته نشده.
     *
     * فاز ۶ آخرین هشت موردش را تعیین تکلیف کرد و اکنون خالی است. خالی بماند:
     * هر افزوده‌ای یعنی جدولی بیرون از هر تضمینی مانده. اگر تصمیم واقعاً به تحلیل
     * بیشتری نیاز دارد، همین‌جا با دلیل ثبتش کن — ولی TenantSchemaCoverageTest
     * خالی‌بودن را اجبار می‌کند تا این کار بی‌صدا نگذرد.
     *
     * @var array<class-string, string>
     */
    public const DEFERRED = [];
}
