<?php

namespace App\Policy\Service;

use App\ClinicService\Entity\ServiceItem;
use App\Doctor\Entity\DoctorAddress;
use App\Policy\Entity\Policy;
use App\Policy\Repository\PolicyRepository;
use App\Policy\ValueObject\PolicyOutcome;

/**
 * انتخاب قانون‌های مرتبط، حل تناقض، و ترکیب اثرها.
 *
 * ## ترتیب حل تناقض (بند ۸ مستند)
 *
 * ۱. **اولویت** بزرگ‌تر
 * ۲. در تساوی: **اختصاصی‌تر** (شعبه بر محیط، سرویس بر دسته)
 * ۳. باز هم تساوی: قانون **قدیمی‌تر**
 *
 * قاعدهٔ سوم عمداً «قدیمی‌تر» است نه «تازه‌تر»: قانونی که مدت‌هاست کار می‌کند رفتار
 * جاافتادهٔ کلینیک است و قانون تازه‌ای که تصادفاً هم‌اولویت شده نباید بی‌صدا عوضش کند.
 *
 * ## ترکیب اثرها
 *
 * از جدول {@see PolicySchema::COMBINATION} می‌آید — `veto`، `max`، `sum`، `union`.
 * یک `forbid` کل عملیات را رد می‌کند حتی اگر ده قانون مجازکننده باشند؛ ممنوعیت رأی
 * اکثریت نیست.
 */
final class PolicyResolver
{
    public function __construct(
        private readonly PolicyRepository    $policies,
        private readonly ConditionEvaluator  $evaluator,
    ) {}

    /**
     * @param array<string, mixed> $facts
     */
    public function resolve(
        string $category,
        string $entityType,
        int $entityId,
        array $facts,
        ?DoctorAddress $address = null,
        ?ServiceItem $service = null,
        ?int $at = null,
    ): PolicyOutcome {
        $at        = $at ?? time();
        $candidates = $this->policies->findForCategory($entityType, $entityId, $category);

        $matched = [];

        foreach ($candidates as $policy) {
            if (!$policy->appliesAt($at) || !$this->inScope($policy, $address, $service)) {
                continue;
            }

            if ($this->evaluator->matches($policy, $facts)) {
                $matched[] = $policy;
            }
        }

        if ($matched === []) {
            return new PolicyOutcome();
        }

        usort($matched, $this->comparator(...));

        return $this->combine($matched);
    }

    /**
     * ارزیابی **یک** قانون، بدون رقابت و بدون ترکیب با بقیه.
     *
     * سؤال آزمایشگاه این است که «این قانون چه می‌کند»، نه «نتیجهٔ نهایی با همهٔ قوانین
     * چه می‌شود». دومی مفید است ولی چیزی نیست که کاربرِ در حال نوشتن قانون می‌پرسد.
     *
     * دامنه و اعتبار زمانی هم عمداً نادیده گرفته می‌شوند: کاربر دارد قانونِ **پیش‌نویس**
     * را روی نمونهٔ گذشته می‌آزماید؛ رد کردنش به‌خاطر اینکه هنوز فعال نیست بی‌معناست.
     *
     * @param array<string, mixed> $facts
     */
    public function evaluateOne(Policy $policy, array $facts): PolicyOutcome
    {
        if (!$this->evaluator->matches($policy, $facts)) {
            return new PolicyOutcome();
        }

        return $this->combine([$policy]);
    }

    /**
     * قانونی که دامنه‌اش با این درخواست نمی‌خواند اصلاً کاندید نیست.
     *
     * دامنهٔ تهی یعنی «همه» — قانون سطح محیط روی همه‌چیز اعمال می‌شود.
     */
    private function inScope(Policy $policy, ?DoctorAddress $address, ?ServiceItem $service): bool
    {
        if ($policy->getAddress() !== null && $policy->getAddress()->getId() !== $address?->getId()) {
            return false;
        }

        if ($policy->getServiceItem() !== null && $policy->getServiceItem()->getId() !== $service?->getId()) {
            return false;
        }

        if ($policy->getCatalogCategory() !== null
            && $policy->getCatalogCategory()->getId() !== $service?->getCatalogCategory()?->getId()
        ) {
            return false;
        }

        return true;
    }

    private function comparator(Policy $a, Policy $b): int
    {
        return [$b->getPriority(), $b->specificity(), $a->getCreatedAt()]
            <=> [$a->getPriority(), $a->specificity(), $b->getCreatedAt()];
    }

    /** @param Policy[] $policies به ترتیب برنده‌ترین */
    private function combine(array $policies): PolicyOutcome
    {
        $effects = [];
        $applied = [];
        $forbids = [];

        foreach ($policies as $policy) {
            $contributed = false;

            foreach ($policy->getEffects() as $effect) {
                $type = $effect['type'] ?? null;

                if (!is_string($type)) {
                    continue;
                }

                $contributed = true;
                $mode        = PolicySchema::COMBINATION[$type] ?? 'max';
                $value       = $effect['value'] ?? true;

                if ($mode === 'veto') {
                    // متن دلخواه با کلید `reason` می‌آید؛ نبودنش خطا نیست چون نام
                    // خودِ قانون همیشه یک توضیح قابل‌فهم است.
                    $reason    = $effect['reason'] ?? null;
                    $forbids[] = is_string($reason) && trim($reason) !== ''
                        ? $reason
                        : sprintf('قانون «%s» این عملیات را مجاز نمی‌داند', $policy->getName());
                    continue;
                }

                $effects[$type] = match ($mode) {
                    'sum'   => (float) ($effects[$type] ?? 0) + (float) $value,
                    'union' => array_values(array_unique([...($effects[$type] ?? []), ...(array) $value])),
                    default => max($effects[$type] ?? $value, $value),   // max
                };
            }

            if ($contributed) {
                $applied[] = [
                    'uuid'    => $policy->getUuid(),
                    'name'    => $policy->getName(),
                    'version' => $policy->getVersion(),
                ];
            }
        }

        // جمع‌ها به عدد صحیح برمی‌گردند: دقیقه و ریال هر دو صحیح‌اند.
        foreach ($effects as $type => $value) {
            if (is_float($value)) {
                $effects[$type] = $value == (int) $value ? (int) $value : $value;
            }
        }

        return new PolicyOutcome($effects, $applied, $forbids);
    }
}
