<?php

namespace App\ClinicService\Service;

use App\ClinicService\Entity\ItemGroup;
use App\ClinicService\Entity\ItemGroupMember;
use App\ClinicService\Entity\ServiceItem;
use App\ClinicService\Entity\ServiceItemRelation;
use App\ClinicService\Repository\ItemGroupMemberRepository;
use App\ClinicService\Repository\ServiceItemRelationRepository;
use App\Doctor\Entity\DoctorAddress;
use App\Policy\Entity\Policy;
use App\Policy\Engine\SelectionPolicyEngine;
use App\ClinicService\Repository\ServiceBranchOverrideRepository;

/**
 * اعتبارسنجی انتخاب کاربر — پیش از هر محاسبهٔ زمان یا قیمت.
 *
 * بند ۵ مستند این قوانین را عمداً از موتور قوانین بیرون می‌گذارد: «حداقل یک ناحیه»
 * یک عدد است نه یک قانون، و «بیکینی با فول‌بادی جمع نمی‌شود» یک رابطه است نه یک شرط.
 * سپردنشان به موتور یعنی هر سرویس چند قانون و هیچ‌کس نمی‌فهمد چرا انتخابش رد شد.
 *
 * خروجی **همهٔ** خطاها را می‌دهد نه اولی: کاربری که سه مشکل دارد نباید سه بار
 * رفت‌وبرگشت کند.
 */
final class ServiceSelectionValidator
{
    public function __construct(
        private readonly ItemGroupMemberRepository       $groupMembers,
        private readonly ServiceItemRelationRepository   $relations,
        private readonly ServiceBranchOverrideRepository $overrides,
        private readonly DurationCalculator              $durations,
        private readonly SelectionPolicyEngine                  $selectionPolicies,
    ) {}

    /**
     * @param ServiceItem[] $selected آیتم‌های انتخاب‌شده، همه از محیط جاری
     * @param ItemGroup[]   $groups   گروه‌های سرویسِ هدف
     * @return array<string, mixed>
     */
    public function validate(array $selected, array $groups, ?DoctorAddress $address = null): array
    {
        $errors = [
            ...$this->groupErrors($selected, $groups),
            ...$this->relationErrors($selected),
            ...$this->policyErrors($selected, $address),
        ];

        $overrides = $address === null
            ? []
            : $this->overrides->mapForAddress(
                array_map(static fn (ServiceItem $i): int => (int) $i->getId(), $selected),
                $address,
            );

        return [
            'valid'                  => $errors === [],
            'errors'                 => $errors,
            'total_duration_minutes' => $this->durations->totalMinutes($selected, $overrides),
            'total_price_rials'      => $this->durations->totalPriceRials($selected, $overrides),
            'breakdown'              => $this->durations->breakdown($selected, $overrides),
        ];
    }

    /**
     * ممنوعیت‌های دستهٔ «انتخاب» — لایه‌ای روی گروه و رابطه، نه جایگزینشان.
     *
     * گروه و رابطه ساختار ثابتِ کاتالوگ‌اند؛ قانون چیزی است که کلینیک بدون دست زدن
     * به کاتالوگ روشن و خاموش می‌کند. بدون شعبه اجرا نمی‌شود چون محیط از آدرس
     * می‌آید و بی‌آن هیچ محیطی برای جست‌وجو نیست.
     *
     * @param ServiceItem[] $selected
     * @return list<array<string, mixed>>
     */
    private function policyErrors(array $selected, ?DoctorAddress $address): array
    {
        if ($address === null || $selected === []) {
            return [];
        }

        $facts = [
            'item_count' => count($selected),
            'item_uuids' => array_map(static fn (ServiceItem $i): string => $i->getUuid(), $selected),
        ];

        $errors = [];

        // هر آیتم جداگانه حل می‌شود: قانونی که دامنه‌اش یک سرویس خاص است فقط وقتی
        // معنا دارد که همان سرویس در انتخاب باشد، و پیام خطا باید بگوید کدام.
        foreach ($selected as $item) {
            $outcome = $this->selectionPolicies->evaluate(
                $address->tenantEntityType(),
                $address->tenantEntityId(),
                $facts + ['catalog_category' => $item->getCatalogCategory()?->getUuid()],
                $address,
                $item,
            );

            foreach ($outcome->forbidReasons as $reason) {
                $errors[] = [
                    'code'    => 'policy_forbidden',
                    'items'   => [$item->getUuid()],
                    'message' => $reason,
                ];
            }
        }

        return $errors;
    }

    /**
     * @param ServiceItem[] $selected
     * @param ItemGroup[]   $groups
     * @return list<array<string, mixed>>
     */
    private function groupErrors(array $selected, array $groups): array
    {
        $selectedIds = array_map(static fn (ServiceItem $i): int => (int) $i->getId(), $selected);
        $errors      = [];

        foreach ($groups as $group) {
            $memberIds = array_map(
                static fn (ItemGroupMember $m): int => (int) $m->getItem()->getId(),
                $group->getMembers()->toArray(),
            );

            $chosen = count(array_intersect($selectedIds, $memberIds));

            if ($chosen < $group->getMinSelect()) {
                $errors[] = [
                    'group_uuid' => $group->getUuid(),
                    'code'       => 'min_select',
                    'message'    => sprintf(
                        'انتخاب حداقل %d مورد از «%s» الزامی است',
                        $group->getMinSelect(),
                        $group->getName(),
                    ),
                ];
            }

            // `max_select = null` یعنی نامحدود — نه صفر.
            if ($group->getMaxSelect() !== null && $chosen > $group->getMaxSelect()) {
                $errors[] = [
                    'group_uuid' => $group->getUuid(),
                    'code'       => 'max_select',
                    'message'    => sprintf(
                        'حداکثر %d مورد از «%s» قابل انتخاب است',
                        $group->getMaxSelect(),
                        $group->getName(),
                    ),
                ];
            }
        }

        return $errors;
    }

    /**
     * @param ServiceItem[] $selected
     * @return list<array<string, mixed>>
     */
    private function relationErrors(array $selected): array
    {
        $byId = [];
        foreach ($selected as $item) {
            $byId[(int) $item->getId()] = $item;
        }

        if ($byId === []) {
            return [];
        }

        $errors = [];
        $seen   = [];

        foreach ($this->relations->findTouching(array_keys($byId)) as $relation) {
            $itemId    = (int) $relation->getItem()->getId();
            $relatedId = (int) $relation->getRelatedItem()->getId();

            if ($relation->getType() === ServiceItemRelation::TYPE_INCOMPATIBLE) {
                // ناسازگاری متقارن است؛ فقط وقتی خطاست که **هر دو** انتخاب شده باشند.
                if (!isset($byId[$itemId], $byId[$relatedId])) {
                    continue;
                }

                // یک جفت، یک خطا — نه دو تا برای دو جهت رابطه.
                $key = $itemId < $relatedId ? "$itemId:$relatedId" : "$relatedId:$itemId";

                if (isset($seen[$key])) {
                    continue;
                }
                $seen[$key] = true;

                $errors[] = [
                    'code'    => 'incompatible',
                    'items'   => [$relation->getItem()->getUuid(), $relation->getRelatedItem()->getUuid()],
                    'message' => sprintf(
                        '«%s» و «%s» در یک نوبت قابل انجام نیستند',
                        $relation->getItem()->getName(),
                        $relation->getRelatedItem()->getName(),
                    ),
                ];

                continue;
            }

            // پیش‌نیاز جهت‌دار است: فقط وقتی معنا دارد که خودِ آیتم انتخاب شده باشد.
            if (!isset($byId[$itemId]) || isset($byId[$relatedId])) {
                continue;
            }

            $errors[] = [
                'code'    => 'missing_prerequisite',
                'items'   => [$relation->getItem()->getUuid(), $relation->getRelatedItem()->getUuid()],
                'message' => sprintf(
                    'برای «%s» ابتدا باید «%s» انتخاب شود',
                    $relation->getItem()->getName(),
                    $relation->getRelatedItem()->getName(),
                ),
            ];
        }

        return $errors;
    }

    /**
     * گروه‌هایی که این آیتم‌ها در آن‌ها عضوند — وقتی کلاینت سرویسِ هدف را نفرستاده.
     *
     * @param ServiceItem[] $selected
     * @return ItemGroup[]
     */
    public function groupsOf(array $selected): array
    {
        $members = $this->groupMembers->findByItemIds(
            array_map(static fn (ServiceItem $i): int => (int) $i->getId(), $selected),
        );

        $groups = [];
        foreach ($members as $member) {
            $groups[(int) $member->getGroup()->getId()] = $member->getGroup();
        }

        return array_values($groups);
    }
}
