<?php

namespace App\Insurance\Service;

use App\ClinicService\Repository\ServiceItemRepository;
use App\Insurance\Entity\TenantInsurance;
use App\Insurance\Entity\TenantInsuranceCategoryCoverage;
use App\Insurance\Entity\TenantServiceCoverage;
use App\Insurance\Enum\InsuranceType;
use App\Insurance\Enum\ServiceCategory;
use App\Insurance\Repository\InsuranceRepository;
use App\Insurance\Repository\TenantInsuranceCategoryCoverageRepository;
use App\Insurance\Repository\TenantInsuranceRepository;
use App\Insurance\Repository\TenantServiceCoverageRepository;
use App\Insurance\ValueObject\CoverageRule;
use App\Shared\Constant\ErrorCodes;
use App\Shared\Exception\AppException;

class TenantInsuranceService
{
    public const SOURCE_SERVICE_OVERRIDE = 'service_override';
    public const SOURCE_OVERRIDE         = 'override';
    public const SOURCE_ADMIN_DEFAULT    = 'admin_default';
    public const SOURCE_CONTRACT         = 'contract';

    public function __construct(
        private readonly TenantInsuranceRepository                 $repo,
        private readonly InsuranceRepository                       $insuranceRepo,
        private readonly TenantServiceCoverageRepository           $coverageRepo,
        private readonly ServiceItemRepository                     $serviceItemRepo,
        private readonly TenantInsuranceCategoryCoverageRepository $categoryCoverageRepo,
        private readonly InsuranceCoverageDefaultService           $coverageDefaults,
        private readonly TenantServiceCategoryService              $serviceCategories,
    ) {}

    /**
     * فعال‌سازی یا به‌روزرسانی قرارداد بیمه برای یک tenant.
     * اگر قرارداد فعالی موجود باشد، همان ویرایش می‌شود؛ در غیر این صورت نسخه‌ی جدید ساخته می‌شود.
     */
    public function activate(
        string $entityType,
        int $entityId,
        int $insuranceId,
        float $coveragePercent,
        float $franchisePercent = 0.0,
        ?int $annualCeilingRials = null,
        ?int $effectiveFrom = null,
        ?int $effectiveTo = null,
        ?string $kind = null,
    ): TenantInsurance {
        $insurance = $this->insuranceRepo->find($insuranceId);
        if ($insurance === null) {
            throw new AppException(ErrorCodes::ERR_NOT_FOUND_001, 'بیمه یافت نشد', 404);
        }

        $contract = $this->repo->findActiveContract($entityType, $entityId, $insuranceId);
        if ($contract === null) {
            $version  = $this->repo->latestVersion($entityType, $entityId, $insuranceId) + 1;
            $contract = new TenantInsurance($entityType, $entityId, $insuranceId, $version);
        }

        $this->assertPercentInRange($franchisePercent, 'فرانشیز باید بین ۰ تا ۱۰۰ باشد', 'franchise_percent');

        $contract->setCoveragePercent($coveragePercent)
            ->setFranchisePercent($franchisePercent)
            ->setAnnualCeilingRials($annualCeilingRials)
            // kind defaults to the catalog type; caller may override to categorise the contract.
            ->setKind($kind ?? $insurance->getType()->value)
            ->setEffectiveTo($effectiveTo)
            ->setActive(true);

        if ($effectiveFrom !== null) {
            $contract->setEffectiveFrom($effectiveFrom);
        }

        $this->repo->save($contract);

        return $contract;
    }

    public function deactivate(TenantInsurance $contract): void
    {
        $contract->setActive(false)->setEffectiveTo(time());
        $this->repo->save($contract);
    }

    // ── Per-category coverage percentages ─────────────────────────────────────

    /**
     * Replaces the contract's category overrides. A row whose percentage is null is
     * dropped, which hands that category back to the central admin default — but only
     * for a service kind the tenant does not cover; an enabled kind must carry a
     * percentage, otherwise the contract would silently bill it at zero.
     *
     * @param list<array{key?: string, coverage_percent?: mixed}> $rows
     * @throws AppException on an unknown category, an out-of-range percentage, or a
     *                      priced-out enabled service kind
     */
    public function setCategoryCoverages(TenantInsurance $contract, array $rows): void
    {
        $this->assertEnabledCategoriesArePriced($contract, $rows);

        foreach ($rows as $row) {
            $category = ServiceCategory::tryFromValue(isset($row['key']) ? (string) $row['key'] : null);
            if ($category === null) {
                throw new AppException(
                    ErrorCodes::ERR_VALIDATION_001,
                    'نوع خدمت نامعتبر است: ' . implode('، ', ServiceCategory::values()),
                    422,
                );
            }

            $existing = $this->categoryCoverageRepo->findOneFor($contract->getId(), $category);
            $raw      = $row['coverage_percent'] ?? null;

            if ($raw === null || $raw === '') {
                if ($existing !== null) {
                    $this->categoryCoverageRepo->remove($existing);
                }
                continue;
            }

            $percent = (float) $raw;
            $this->assertPercentInRange($percent, 'درصد پوشش باید بین ۰ تا ۱۰۰ باشد');

            $entity = $existing ?? new TenantInsuranceCategoryCoverage($contract->getId(), $category);
            $this->categoryCoverageRepo->save($entity->setCoveragePercent($percent), false);
        }

        $this->categoryCoverageRepo->flush();
    }

    /**
     * هر نوع خدمتی که tenant آن را بیمه‌ای کرده باید در پایانِ این ذخیره‌سازی درصد
     * پوشش مؤثر داشته باشد — از خودِ payload، از override قبلی، یا از پیش‌فرض مرکزی
     * ادمین. fallback زنده حفظ می‌شود؛ چیزی که رد می‌شود قراردادی است که نوع خدمتِ
     * فعال را عملاً صفر درصد می‌کند.
     *
     * نیامدنِ کلید `category_coverages` اصلاً به اینجا نمی‌رسد — آن حالت یعنی
     * «قرارداد دست‌نخورده روی همان مسیر resolve بماند».
     *
     * @param list<array{key?: string, coverage_percent?: mixed}> $rows
     * @throws AppException وقتی نوع خدمتِ فعالی بدون درصد مؤثر بماند
     */
    private function assertEnabledCategoriesArePriced(TenantInsurance $contract, array $rows): void
    {
        $sent = [];
        foreach ($rows as $row) {
            $sent[(string) ($row['key'] ?? '')] = $row['coverage_percent'] ?? null;
        }

        $overrides = $this->categoryCoverageRepo->percentMapFor($contract->getId());
        $defaults  = $this->coverageDefaults->percentMap($contract->getInsuranceId());

        foreach ($this->serviceCategories->enabledKeys($contract->getEntityType(), $contract->getEntityId()) as $key) {
            // ارسال صریحِ null یعنی «override را بردار»، پس نباید خودِ همان override
            // که همین حالا حذف می‌شود، اعتبارسنجی را نجات بدهد.
            // ستون قدیمیِ coverage_percent قرارداد عمداً fallback حساب نمی‌شود: با آن،
            // نوع خدمتی که درصدش نیامده بی‌صدا نرخ نوع دیگر را ارث می‌برد.
            $percent = match (true) {
                array_key_exists($key, $sent) && $sent[$key] !== null && $sent[$key] !== '' => (float) $sent[$key],
                array_key_exists($key, $sent)                                               => $defaults[$key] ?? 0.0,
                default                                                                     => $overrides[$key] ?? $defaults[$key] ?? 0.0,
            };

            if ($percent <= 0) {
                throw new AppException(
                    ErrorCodes::ERR_VALIDATION_001,
                    sprintf('درصد پوشش %s الزامی است', ServiceCategory::from($key)->label()),
                    422,
                    'category_coverages',
                );
            }
        }
    }

    /** @throws AppException وقتی درصد بیرون از بازهٔ ۰ تا ۱۰۰ باشد */
    private function assertPercentInRange(float $percent, string $message, ?string $field = null): void
    {
        if ($percent < 0 || $percent > 100) {
            throw new AppException(ErrorCodes::ERR_VALIDATION_001, $message, 422, $field);
        }
    }

    /**
     * Effective percentage per category plus where each value came from, so the panel
     * can tell an explicit override apart from an inherited central default.
     *
     * @return array{category_coverages: array<string, float>, category_coverage_source: array<string, string>}
     */
    public function categoryCoverageView(TenantInsurance $contract, ?array $overrides = null, ?array $defaults = null): array
    {
        $overrides ??= $this->categoryCoverageRepo->percentMapFor($contract->getId());
        $defaults  ??= $this->coverageDefaults->percentMap($contract->getInsuranceId());

        $percents = [];
        $sources  = [];
        foreach (ServiceCategory::cases() as $category) {
            [$percent, $source] = $this->pickPercent($contract, $category, $overrides, $defaults);
            $percents[$category->value] = $percent;
            $sources[$category->value]  = $source;
        }

        return ['category_coverages' => $percents, 'category_coverage_source' => $sources];
    }

    /**
     * Same as categoryCoverageView() for a whole list, resolved in two queries.
     *
     * @param TenantInsurance[] $contracts
     * @return array<int, array{category_coverages: array<string, float>, category_coverage_source: array<string, string>}>
     */
    public function categoryCoverageViewForMany(array $contracts): array
    {
        $overrides = $this->categoryCoverageRepo->percentMapForMany(
            array_map(static fn(TenantInsurance $c) => (int) $c->getId(), $contracts)
        );
        $defaults = $this->coverageDefaults->percentMapForMany(
            array_values(array_unique(array_map(static fn(TenantInsurance $c) => $c->getInsuranceId(), $contracts)))
        );

        $view = [];
        foreach ($contracts as $contract) {
            $view[(int) $contract->getId()] = $this->categoryCoverageView(
                $contract,
                $overrides[$contract->getId()] ?? [],
                $defaults[$contract->getInsuranceId()] ?? [],
            );
        }

        return $view;
    }

    /**
     * درصد پوشش مؤثر یک نوع خدمت، به ترتیب اولویت:
     *   ۱) override قرارداد برای همان نوع خدمت (TenantInsuranceCategoryCoverage)
     *   ۲) پیش‌فرض مرکزی ادمین (InsuranceCoverageDefault) — تنها وقتی تعریف شده باشد
     *   ۳) coverage_percent قرارداد (سازگاری با ردیف‌های قدیمی)
     *
     * @param array<string, float> $overrides
     * @param array<string, float> $defaults
     * @return array{0: float, 1: string}
     */
    private function pickPercent(
        TenantInsurance $contract,
        ServiceCategory $category,
        array $overrides,
        array $defaults,
    ): array {
        if (isset($overrides[$category->value])) {
            return [$overrides[$category->value], self::SOURCE_OVERRIDE];
        }

        // A stored 0 means "not configured yet" (rows are pre-seeded for every
        // insurance), so it must not shadow a legacy contract percentage.
        $default = $defaults[$category->value] ?? 0.0;
        if ($default > 0) {
            return [$default, self::SOURCE_ADMIN_DEFAULT];
        }

        return [$contract->getCoveragePercent(), self::SOURCE_CONTRACT];
    }

    /**
     * بررسی فعال‌بودن یک بیمه برای tenant. در پذیرش/صورتحساب استفاده می‌شود.
     */
    public function assertActive(string $entityType, int $entityId, int $insuranceId): TenantInsurance
    {
        $contract = $this->repo->findActiveContract($entityType, $entityId, $insuranceId);
        if ($contract === null) {
            throw new AppException(ErrorCodes::ERR_VALIDATION_001, 'این بیمه برای این کلینیک/پزشک فعال نیست', 422);
        }
        return $contract;
    }

    /**
     * قانون پوشش ویزیت برای tenant جاری (برای BillingCalculator). درصد بر پایهٔ نوع
     * خدمتِ داده‌شده resolve می‌شود؛ پیش‌فرض سرپایی است چون ویزیت آیتم سرویس نیست.
     * اگر قرارداد فعالی نباشد، notCovered برمی‌گردد.
     */
    public function coverageRule(
        string $entityType,
        int $entityId,
        ?int $insuranceId,
        ServiceCategory $category = ServiceCategory::Outpatient,
    ): CoverageRule {
        if ($insuranceId === null) {
            return CoverageRule::notCovered();
        }

        $contract = $this->repo->findActiveContract($entityType, $entityId, $insuranceId);
        if ($contract === null) {
            return CoverageRule::notCovered();
        }

        return $this->buildRule($contract, $category, null);
    }

    /**
     * قانون پوشش یک خدمت خاص تحت بیمه‌ی tenant.
     * اگر override خدمت موجود باشد اعمال می‌شود؛ فیلدهای null از قرارداد ارث می‌برند.
     */
    public function coverageRuleForService(string $entityType, int $entityId, ?int $insuranceId, int $serviceItemId): CoverageRule
    {
        if ($insuranceId === null) {
            return CoverageRule::notCovered();
        }

        // پرچم سرویس gate نهایی است: اگر «این خدمت شامل بیمه می‌شود» غیرفعال باشد، هیچ پوششی محاسبه نمی‌شود.
        $service = $this->serviceItemRepo->find($serviceItemId);
        if ($service === null || !$service->isInsuranceCovered()) {
            return CoverageRule::notCovered();
        }

        $contract = $this->repo->findActiveContract($entityType, $entityId, $insuranceId);
        if ($contract === null) {
            return CoverageRule::notCovered();
        }

        $override = $this->coverageRepo->findOneFor($contract->getId(), $serviceItemId);
        if ($override !== null && !$override->isCovered()) {
            return CoverageRule::notCovered();
        }

        return $this->buildRule($contract, $service->getServiceCategory(), $override);
    }

    /**
     * ساخت قاعدهٔ پوشش با درصد resolve‌شده. فرانشیز فقط در قرارداد تکمیلی اثر دارد؛
     * در بیمهٔ پایه قاعده صرفاً درصدی است (ستون DB برای سازگاری باقی می‌ماند).
     */
    private function buildRule(
        TenantInsurance $contract,
        ServiceCategory $category,
        ?TenantServiceCoverage $override,
    ): CoverageRule {
        $franchise = $this->isSupplementary($contract)
            ? ($override?->getFranchisePercent() ?? $contract->getFranchisePercent())
            : 0.0;

        return new CoverageRule(
            coveragePercent:  $this->resolvePercent($contract, $category, $override),
            franchisePercent: $franchise,
            ceilingRials:     $override?->getCeilingRials() ?? $contract->getAnnualCeilingRials(),
            covered:          true,
        );
    }

    /**
     * درصد پوشش مؤثر، به ترتیب اولویت:
     *   ۱) override همان خدمت (TenantServiceCoverage.coverage_percent)
     *   ۲) override قرارداد برای نوع خدمت (TenantInsuranceCategoryCoverage)
     *   ۳) پیش‌فرض مرکزی ادمین (InsuranceCoverageDefault)
     *   ۴) coverage_percent قرارداد (سازگاری با ردیف‌های قدیمی)
     */
    public function resolvePercent(
        TenantInsurance $contract,
        ServiceCategory $category,
        ?TenantServiceCoverage $override = null,
    ): float {
        $servicePercent = $override?->getCoveragePercent();
        if ($servicePercent !== null) {
            return $servicePercent;
        }

        [$percent] = $this->pickPercent(
            $contract,
            $category,
            $this->categoryCoverageRepo->percentMapFor($contract->getId()),
            $this->coverageDefaults->percentMap($contract->getInsuranceId()),
        );

        return $percent;
    }

    /** قرارداد تکمیلی است؟ kind قرارداد بر نوع کاتالوگ اولویت دارد. */
    private function isSupplementary(TenantInsurance $contract): bool
    {
        $kind = $contract->getKind()
            ?? $this->insuranceRepo->find($contract->getInsuranceId())?->getType()->value;

        return $kind === InsuranceType::Supplementary->value;
    }

    /** @return array{covered: bool, coverage_percent: float|null, franchise_percent: float|null, ceiling_rials: int|null}|null */
    public function getServiceCoverage(int $tenantInsuranceId, int $serviceItemId): ?array
    {
        $override = $this->coverageRepo->findOneFor($tenantInsuranceId, $serviceItemId);
        return $override?->toArray();
    }

    public function setServiceCoverage(
        TenantInsurance $contract,
        int $serviceItemId,
        bool $covered,
        ?float $coveragePercent,
        ?float $franchisePercent,
        ?int $ceilingRials,
    ): void {
        if ($coveragePercent !== null) {
            $this->assertPercentInRange($coveragePercent, 'درصد پوشش باید بین ۰ تا ۱۰۰ باشد', 'coverage_percent');
        }
        if ($franchisePercent !== null) {
            $this->assertPercentInRange($franchisePercent, 'فرانشیز باید بین ۰ تا ۱۰۰ باشد', 'franchise_percent');
        }

        $override = $this->coverageRepo->findOneFor($contract->getId(), $serviceItemId)
            ?? new TenantServiceCoverage($contract->getId(), $serviceItemId);

        $override->setCovered($covered)
            ->setCoveragePercent($coveragePercent)
            ->setFranchisePercent($franchisePercent)
            ->setCeilingRials($ceilingRials);

        $this->coverageRepo->save($override);
        $this->syncServiceItemInsuranceFlag($serviceItemId);
    }

    /**
     * پرچم insurance_covered خدمت را با پوشش‌های واقعی هم‌تراز می‌کند. پنل دیگر این
     * پرچم را دستی نمی‌گیرد؛ تنها منبع حقیقت، ردیف‌های پوشش قراردادهای بیمه است.
     */
    private function syncServiceItemInsuranceFlag(int $serviceItemId): void
    {
        $serviceItem = $this->serviceItemRepo->find($serviceItemId);
        if ($serviceItem === null) {
            return;
        }

        $covered = $this->coverageRepo->hasActiveCoverage($serviceItemId);
        if ($serviceItem->isInsuranceCovered() !== $covered) {
            $this->serviceItemRepo->save($serviceItem->setInsuranceCovered($covered));
        }
    }
}
