<?php

namespace App\Pricing\Service;

use App\ClinicService\Entity\ServiceItem;
use App\ClinicService\Repository\ServiceBranchOverrideRepository;
use App\ClinicService\Repository\TariffRepository;
use App\Doctor\Entity\DoctorAddress;
use App\Package\Service\PackageConsumptionService;
use App\Patient\Entity\PatientRecord;
use App\Pricing\Repository\PriceListItemRepository;
use App\Pricing\Repository\PriceListRepository;
use App\Pricing\ValueObject\PriceQuote;
use App\Policy\Entity\Policy;
use App\Policy\Engine\PricingPolicyEngine;
use App\Policy\Service\PolicySchema;
use App\Representation\Service\JalaliDateService;

/**
 * زنجیرهٔ قیمت‌گذاری بند ۱۲ مستند.
 *
 * ```
 * قیمت پایه  →  + آیتم‌ها  →  − تخفیف  →  − بیمهٔ پایه  →  − تکمیلی  →  + مالیات  →  بیعانه
 * ```
 *
 * ## زنجیرهٔ منبع قیمت
 *
 * برای هر سرویس، اولین چیزی که پیدا شود برنده است:
 *
 * ۱. override شعبه ({@see \App\ClinicService\Entity\ServiceBranchOverride}) — تسک ۰۴
 * ۲. لیست قیمتِ حاکم بر آن تاریخ — همین تسک
 * ۳. `Tariff` سال — لایهٔ موجود
 * ۴. `ServiceItem::priceRials` — همیشه هست
 *
 * مرحلهٔ چهارم ضامن است که **هرگز صفر یا خطا** برنگردد: تاریخی که هیچ لیستی نمی‌پوشاند
 * باید قیمت بدهد، نه استثنا.
 */
final class PricingEngine
{
    public function __construct(
        private readonly PricingPolicyEngine                  $pricingPolicies,
        private readonly PackageConsumptionService       $packages,
        private readonly PriceListRepository             $priceLists,
        private readonly PriceListItemRepository         $priceListItems,
        private readonly ServiceBranchOverrideRepository $overrides,
        private readonly TariffRepository                $tariffs,
        private readonly JalaliDateService               $jalali,
    ) {}

    /**
     * @param ServiceItem[] $items آیتم‌های انتخاب‌شده (بدون خودِ سرویس)
     * @param array{
     *     discount_percent?: float, discount_rials?: int, discount_label?: string,
     *     max_total_discount_percent?: float,
     *     insurance_base_percent?: float, insurance_supplementary_percent?: float,
     *     tax_percent?: float, deposit_percent?: float, deposit_rials?: int
     * } $policy
     */
    public function quote(
        ServiceItem $service,
        array $items,
        DoctorAddress $address,
        int $at,
        array $policy = [],
        ?PatientRecord $patient = null,
    ): PriceQuote {
        $entityType = $address->tenantEntityType();
        $entityId   = $address->tenantEntityId();

        $list = $this->priceLists->findCovering($entityType, $entityId, $address, $at);

        $sources = [];

        $base       = $this->priceFor($service, $address, $list, $at, $sources);
        $itemsTotal = 0;

        foreach ($items as $item) {
            $itemsTotal += $this->priceFor($item, $address, $list, $at, $sources);
        }

        $subtotal = $base + $itemsTotal;

        // ── پکیج ──────────────────────────────────────────────────────────────
        // پکیج **قیمت پایهٔ سرویس** را می‌پوشاند، نه آیتم‌های اضافه: «شش جلسه لیزر»
        // یعنی شش بار خودِ لیزر، نه هر چیزی که کنارش انتخاب شود.
        $usable = $patient === null ? null : $this->packages->firstUsable($patient, $service, $at);
        $covered = 0;

        if ($usable !== null) {
            $covered  = min($base, $subtotal);
            $subtotal -= $covered;
        }

        // ── تخفیف ─────────────────────────────────────────────────────────────
        // قوانین دستهٔ «قیمت» کنار سیاست دستیِ درخواست می‌نشینند، نه به‌جایش: تخفیفی
        // که اپراتور دستی می‌دهد و تخفیفی که قانون می‌دهد هر دو واقعی‌اند.
        $policy = $this->mergePolicyDiscounts($service, $items, $address, $at, $subtotal, $policy, $sources);

        [$discount, $discounts] = $this->discountFor($subtotal, $policy);

        // تخفیف بیشتر از مبلغ، مبلغ را **صفر** می‌کند نه منفی: بدهی منفی یعنی کلینیک
        // به بیمار پول بدهکار شود، که هیچ‌جای این جریان معنا ندارد.
        $discount    = min($discount, $subtotal);
        $afterDiscount = $subtotal - $discount;

        // ── بیمه ──────────────────────────────────────────────────────────────
        $insuranceBase = $this->percentOf($afterDiscount, $policy['insurance_base_percent'] ?? 0.0);
        $insuranceBase = min($insuranceBase, $afterDiscount);

        $remaining     = $afterDiscount - $insuranceBase;
        $supplementary = min($this->percentOf($remaining, $policy['insurance_supplementary_percent'] ?? 0.0), $remaining);

        $patientShare = $remaining - $supplementary;

        // ── مالیات ────────────────────────────────────────────────────────────
        // روی سهم بیمار حساب می‌شود، نه روی کل: بیمار مالیاتِ سهمی که بیمه می‌دهد را
        // نمی‌پردازد.
        $tax   = $this->percentOf($patientShare, $policy['tax_percent'] ?? 0.0);
        $final = $patientShare + $tax;

        // ── بیعانه ────────────────────────────────────────────────────────────
        $deposit = isset($policy['deposit_rials'])
            ? (int) $policy['deposit_rials']
            : $this->percentOf($final, $policy['deposit_percent'] ?? 0.0);

        $deposit = max(0, min($deposit, $final));

        if ($covered > 0) {
            $discounts[] = [
                'label' => sprintf('پوشش پکیج «%s»', $usable?->getPackage()->getName() ?? '—'),
                'rials' => $covered,
                'kind'  => 'package',
            ];
        }

        return new PriceQuote(
            baseRials: $base,
            itemsRials: $itemsTotal,
            discountRials: $discount,
            insuranceBaseRials: $insuranceBase,
            insuranceSupplementaryRials: $supplementary,
            taxRials: $tax,
            finalRials: $final,
            depositRials: $deposit,
            discounts: $discounts,
            sources: $sources,
            packageWillBeConsumed: $usable !== null,
            packageUuid: $usable?->getUuid(),
        );
    }

    /**
     * اثر قوانین «قیمت» را به سیاست درخواست اضافه می‌کند.
     *
     * شناسه و **نسخهٔ** هر قانون در `sources` ثبت می‌شود تا فاکتور بتواند سه ماه بعد
     * بگوید کدام نسخه رویش اعمال شده بود.
     *
     * @param ServiceItem[]        $items
     * @param array<string, mixed> $policy
     * @param array<string, mixed> $sources
     * @return array<string, mixed>
     */
    private function mergePolicyDiscounts(
        ServiceItem $service,
        array $items,
        DoctorAddress $address,
        int $at,
        int $subtotal,
        array $policy,
        array &$sources,
    ): array {
        $outcome = $this->pricingPolicies->evaluate(
            $address->tenantEntityType(),
            $address->tenantEntityId(),
            [
                'item_count'     => count($items),
                'subtotal_rials' => $subtotal,
                'patient_tags'   => $policy['patient_tags'] ?? [],
                'visit_count'    => $policy['visit_count'] ?? 0,
            ],
            $address,
            $service,
            $at,
        );

        if ($outcome->appliedPolicies === []) {
            return $policy;
        }

        $sources['applied_policies'] = $outcome->appliedPolicies;

        $policy['discount_percent'] = (float) ($policy['discount_percent'] ?? 0)
            + (float) $outcome->effect(PolicySchema::EFFECT_DISCOUNT_PERCENT, 0);

        $policy['discount_rials'] = (int) ($policy['discount_rials'] ?? 0)
            + (int) $outcome->effect(PolicySchema::EFFECT_DISCOUNT_RIALS, 0);

        $policy['discount_label'] ??= $outcome->appliedPolicies[0]['name'];

        return $policy;
    }

    /**
     * @param array<string, string> $sources
     */
    private function priceFor(
        ServiceItem $service,
        DoctorAddress $address,
        ?\App\Pricing\Entity\PriceList $list,
        int $at,
        array &$sources,
    ): int {
        $override = $this->overrides->mapForAddress([(int) $service->getId()], $address)[(int) $service->getId()] ?? null;

        if ($override?->getPriceRials() !== null) {
            $sources[$service->getUuid()] = 'branch_override';

            return $override->getPriceRials();
        }

        if ($list !== null) {
            $price = $this->priceListItems->priceMap($list, [$service])[(int) $service->getId()] ?? null;

            if ($price !== null) {
                $sources[$service->getUuid()] = 'price_list';

                return $price;
            }
        }

        $tariff = $this->tariffs->findForServiceYear((int) $service->getId(), $this->jalali->jalaliYear($at));

        if ($tariff !== null) {
            $sources[$service->getUuid()] = 'tariff';

            return (int) $tariff->getPriceRials();
        }

        $sources[$service->getUuid()] = 'service_item';

        return $service->getPriceRials();
    }

    /**
     * @param array<string, mixed> $policy
     * @return array{0: int, 1: list<array<string, mixed>>}
     */
    private function discountFor(int $subtotal, array $policy): array
    {
        $discounts = [];
        $total     = 0;

        if (($policy['discount_percent'] ?? 0.0) > 0) {
            $amount = $this->percentOf($subtotal, (float) $policy['discount_percent']);
            $total += $amount;

            $discounts[] = [
                'label'   => $policy['discount_label'] ?? 'تخفیف درصدی',
                'percent' => $policy['discount_percent'],
                'rials'   => $amount,
            ];
        }

        if (($policy['discount_rials'] ?? 0) > 0) {
            $amount = (int) $policy['discount_rials'];
            $total += $amount;

            $discounts[] = [
                'label' => $policy['discount_label'] ?? 'تخفیف مبلغی',
                'rials' => $amount,
            ];
        }

        // سقف جمع تخفیف‌ها per محیط: چند تخفیفِ جداگانه که هرکدام منطقی‌اند، با هم
        // می‌توانند مبلغ را بی‌معنا کنند.
        $cap = $policy['max_total_discount_percent'] ?? null;

        if ($cap !== null && $cap >= 0) {
            $maxAllowed = $this->percentOf($subtotal, (float) $cap);

            if ($total > $maxAllowed) {
                $discounts[] = [
                    'label' => sprintf('سقف تخفیف %s٪ اعمال شد', $cap),
                    'rials' => $maxAllowed - $total,
                ];
                $total = $maxAllowed;
            }
        }

        return [$total, $discounts];
    }

    /** ریال واحد صحیح است؛ گرد کردن به پایین از اضافه‌گرفتن جلوگیری می‌کند. */
    private function percentOf(int $amount, float $percent): int
    {
        if ($percent <= 0) {
            return 0;
        }

        return (int) floor($amount * $percent / 100);
    }
}
