<?php

namespace App\Policy\Simulation;

use App\Appointment\Entity\Appointment;
use App\Auth\Entity\User;
use App\Policy\Entity\Policy;
use App\Policy\Entity\PolicySimulationRun;
use App\Policy\Repository\PolicySimulationRunRepository;
use App\Policy\Service\PolicyResolver;
use App\Policy\Service\PolicySchema;
use App\Policy\ValueObject\PolicyOutcome;
use Doctrine\ORM\EntityManagerInterface;

/**
 * اجرای آزمایشی یک قانون روی نوبت‌های واقعیِ گذشته — بدون نوشتن هیچ چیز.
 *
 * ## تضمین «چیزی ثبت نمی‌شود»، سه لایه
 *
 * ۱. ارزیابی روی **حقایق** انجام می‌شود نه روی entity؛ هیچ entity ای تغییر نمی‌کند.
 * ۲. کل اجرا داخل تراکنشی است که در `finally` **همیشه** rollback و `clear` می‌شود —
 *    حتی اگر روزی کسی سهواً یک `flush` اضافه کند.
 * ۳. `PolicySimulationRunTest` تعداد ردیف جدول‌های حساس را قبل و بعد می‌شمارد.
 *
 * ثبت خودِ `PolicySimulationRun` **بعد** از این بلوک و در تراکنش خودش انجام می‌شود.
 */
final class PolicySimulator
{
    public function __construct(
        private readonly SimulationSampler             $sampler,
        private readonly SimulationFacts               $facts,
        private readonly PolicyResolver                $policies,
        private readonly PolicySimulationRunRepository $runs,
        private readonly EntityManagerInterface        $em,
    ) {}

    public function simulate(Policy $policy, int $size = SimulationSampler::DEFAULT_SIZE, ?User $runBy = null): PolicySimulationRun
    {
        $this->em->beginTransaction();

        try {
            $report = $this->runInternal($policy, $size);
        } finally {
            $this->em->rollback();
            // بدون `clear`، entity های لمس‌شده در identity map می‌مانند و اولین flushِ
            // بعدی در همین request آن‌ها را ثبت می‌کند — باگی که پیدا کردنش روزها می‌برد.
            $this->em->clear();
        }

        // `clear` ارجاع‌های قبلی را از EM جدا کرده؛ قانون باید دوباره خوانده شود.
        $policy = $this->em->getRepository(Policy::class)->find($policy->getId());

        if ($policy === null) {
            throw new \LogicException('Policy vanished during simulation.');
        }

        $run = new PolicySimulationRun(
            $policy,
            $report['sample_size'],
            count($report['rows']),
            PolicySimulationRun::severityFor($report['sample_size'], count($report['rows'])),
            ['rows' => $report['rows'], 'warning' => $report['warning']],
            $runBy === null ? null : $this->em->getRepository(User::class)->find($runBy->getId()),
        );

        $this->runs->save($run);

        return $run;
    }

    /**
     * @return array{sample_size: int, rows: list<array<string, mixed>>, warning: string|null}
     */
    private function runInternal(Policy $policy, int $size): array
    {
        $sample = $this->sampler->recentAppointments($policy, $size);

        if ($sample === []) {
            // کلینیک تازه هیچ نوبت گذشته‌ای ندارد؛ اگر این حالت خطا بود، هرگز
            // نمی‌توانست قانونی فعال کند.
            return ['sample_size' => 0, 'rows' => [], 'warning' => 'داده‌ای برای آزمایش نیست'];
        }

        $rows = [];

        foreach ($sample as $appointment) {
            $outcome = $this->policies->evaluateOne(
                $policy,
                $this->facts->forAppointment($appointment, $policy->getCategory()),
            );

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

            $row = $this->describe($policy, $appointment, $outcome);

            if ($row !== null) {
                $rows[] = $row;
            }
        }

        return ['sample_size' => count($sample), 'rows' => $rows, 'warning' => null];
    }

    /**
     * تفاوت «وضعیت فعلی → با این قانون» به زبان کاربر.
     *
     * تنها ستونی است که کاربر غیرفنی می‌فهمد، پس عمداً متن است نه ساختار خام اثر.
     *
     * @return array<string, mixed>|null `null` یعنی این نوبت عملاً تغییری نمی‌کرد
     */
    private function describe(Policy $policy, Appointment $appointment, PolicyOutcome $outcome): ?array
    {
        $base = [
            'appointment_uuid' => $appointment->getUuid(),
            'patient_name'     => $appointment->getPatientName() ?? '—',
            'slot_start'       => $appointment->getSlotStart(),
        ];

        if ($outcome->isForbidden()) {
            return $base + [
                'before' => 'مجاز',
                'after'  => 'رد می‌شد',
                'reason' => implode(' ', $outcome->forbidReasons),
            ];
        }

        return match ($policy->getCategory()) {
            Policy::CATEGORY_TIMING   => $this->describeTiming($base, $appointment, $outcome),
            Policy::CATEGORY_PRICING  => $this->describePricing($base, $appointment, $outcome),
            Policy::CATEGORY_RESOURCE => $this->describeList(
                $base,
                'منبع لازم',
                (array) $outcome->effect(PolicySchema::EFFECT_REQUIRE_RESOURCE, []),
            ),
            Policy::CATEGORY_ELIGIBILITY => $this->describeList(
                $base,
                'تأیید لازم',
                (array) $outcome->effect(PolicySchema::EFFECT_REQUIRE_FLAG, []),
            ),
            Policy::CATEGORY_SPACING => $this->describeSpacing($base, $outcome),
            default                  => null,
        };
    }

    /**
     * @param array<string, mixed> $base
     * @return array<string, mixed>|null
     */
    private function describeTiming(array $base, Appointment $appointment, PolicyOutcome $outcome): ?array
    {
        $current = $this->facts->durationOf($appointment);
        $target  = max(
            $current + (int) $outcome->effect(PolicySchema::EFFECT_ADD_DURATION, 0),
            (int) $outcome->effect(PolicySchema::EFFECT_MIN_DURATION, 0),
        );

        if ($target === $current) {
            return null;
        }

        return $base + [
            'before' => sprintf('%d دقیقه', $current),
            'after'  => sprintf('%d دقیقه', $target),
            'reason' => sprintf('%+d دقیقه', $target - $current),
        ];
    }

    /**
     * @param array<string, mixed> $base
     * @return array<string, mixed>|null
     */
    private function describePricing(array $base, Appointment $appointment, PolicyOutcome $outcome): ?array
    {
        $subtotal = $this->facts->subtotalOf($appointment);

        $discount = (int) floor($subtotal * (float) $outcome->effect(PolicySchema::EFFECT_DISCOUNT_PERCENT, 0) / 100)
            + (int) $outcome->effect(PolicySchema::EFFECT_DISCOUNT_RIALS, 0);

        $discount = min($discount, $subtotal);

        if ($discount <= 0) {
            return null;
        }

        return $base + [
            'before' => sprintf('%s ریال', number_format($subtotal)),
            'after'  => sprintf('%s ریال', number_format($subtotal - $discount)),
            'reason' => sprintf('%s ریال تخفیف', number_format($discount)),
        ];
    }

    /**
     * @param array<string, mixed> $base
     * @param array<int, mixed>    $values
     * @return array<string, mixed>|null
     */
    private function describeList(array $base, string $label, array $values): ?array
    {
        $values = array_values(array_filter($values, 'is_string'));

        if ($values === []) {
            return null;
        }

        return $base + [
            'before' => 'بدون قید',
            'after'  => sprintf('%s: %s', $label, implode('، ', $values)),
            'reason' => $label,
        ];
    }

    /**
     * @param array<string, mixed> $base
     * @return array<string, mixed>|null
     */
    private function describeSpacing(array $base, PolicyOutcome $outcome): ?array
    {
        $days = (int) $outcome->effect(PolicySchema::EFFECT_MIN_DAYS_BETWEEN, 0);

        if ($days <= 0) {
            return null;
        }

        return $base + [
            'before' => 'بدون حداقل فاصله',
            'after'  => sprintf('حداقل %d روز فاصله', $days),
            'reason' => sprintf('%d روز', $days),
        ];
    }
}
