<?php

namespace App\Payment\Service;

use App\Appointment\Entity\Appointment;
use App\Clinic\Repository\ClinicRepository;
use App\Doctor\Repository\DoctorRepository;
use App\Payment\Entity\Payment;
use App\Payment\Entity\PaymentLog;
use App\Payment\Gateway\GatewayFactory;
use App\Payment\Gateway\PaymentInitResult;
use App\Payment\Gateway\PaymentRefundResult;
use App\Payment\Repository\PaymentLogRepository;
use App\Payment\Repository\PaymentRepository;
use App\Representation\Service\JalaliDateService;
use App\Settlement\Service\CommissionService;
use App\Sms\Entity\SmsLog;
use App\Sms\Service\SmsService;
use App\Sms\Service\SmsWalletService;
use App\Subscription\Service\SubscriptionService;
use Doctrine\ORM\EntityManagerInterface;
use Psr\Log\LoggerInterface;

/**
 * تمام منطق پرداخت (ارتباط با بانک + verify + post-action) اینجاست تا کنترلر فقط
 * Orchestration کند. verify امن داخل transaction با قفل بدبینانه انجام می‌شود.
 */
final class PaymentManager
{
    public function __construct(
        private readonly GatewayFactory $gateways,
        private readonly PaymentRepository $paymentRepo,
        private readonly PaymentLogRepository $paymentLogRepo,
        private readonly CircuitBreakerService $circuitBreaker,
        private readonly EntityManagerInterface $em,
        private readonly LoggerInterface $logger,
        private readonly SubscriptionService $subscriptionService,
        private readonly SmsWalletService $smsWalletService,
        private readonly SmsService $smsService,
        private readonly DoctorRepository $doctorRepo,
        private readonly ClinicRepository $clinicRepo,
        private readonly CommissionService $commissionService,
        private readonly \App\Representation\Service\DomainContextResolver $domainResolver,
        private readonly JalaliDateService $jalali,
        private readonly string $appBaseUrl,
    ) {}

    /**
     * نماینده‌ی مالکِ دامنه‌ای که خرید از آن انجام شده — مبنای کمیسیون دامنه‌محور.
     * frontend_address هنگام initiate با allow-list دامنه‌ها validate شده است.
     */
    private function bookingRepresentationIdFor(Payment $payment): ?int
    {
        return $this->domainResolver->resolve($payment->getFrontendAddress())->representationId();
    }

    /**
     * درگاه را برای یک پرداخت pending init می‌کند (ارتباط با بانک).
     * موفق → PaymentInitResult؛ ناموفق → false (پرداخت failed و ذخیره‌شده).
     */
    public function startGatewayHandoff(Payment $payment): PaymentInitResult|false
    {
        $gatewayName = $payment->getGateway();
        $gateway     = $this->gateways->resolve($gatewayName);
        $testMode    = $this->gateways->isTestMode();

        if ($gateway === null || (!$testMode && $this->circuitBreaker->isOpen($gatewayName))) {
            $this->failPayment($payment, PaymentLog::ACTION_INITIATE, ['reason' => 'gateway_unavailable']);
            return false;
        }

        // درگاه ملت orderId عددی (long) می‌خواهد؛ id عددیِ Payment را می‌فرستیم.
        // جستجوی پرداخت در callback از طریق query `order_id` (رشتهٔ ORD-…) انجام می‌شود.
        $callbackUrl = $this->callbackUrl($payment);
        $result      = $gateway->initiate($payment->getAmountRials(), (string) $payment->getId(), $callbackUrl);

        if (!$result->success) {
            if (!$testMode) {
                $this->circuitBreaker->recordFailure($gatewayName);
            }
            $this->failPayment($payment, PaymentLog::ACTION_INITIATE, ['error' => $result->errorMessage]);
            return false;
        }

        if (!$testMode) {
            $this->circuitBreaker->recordSuccess($gatewayName);
        }
        $payment->setGatewayToken($result->token);
        $this->paymentRepo->save($payment);
        $this->log($payment, PaymentLog::ACTION_INITIATE, 'success', $result->token, null, ['token' => $result->token]);

        return $result;
    }

    /**
     * verify امنِ callback داخل transaction + قفل ردیف. idempotent: اگر پرداخت
     * قبلاً نهایی شده باشد بدون پردازش دوباره همان را برمی‌گرداند.
     * null یعنی پرداخت یافت نشد.
     */
    public function processCallback(string $gatewayName, array $callbackData, string $clientIp, string $orderId): ?Payment
    {
        return $this->em->wrapInTransaction(function () use ($gatewayName, $callbackData, $clientIp, $orderId): ?Payment {
            $payment = $this->paymentRepo->findByOrderIdForUpdate($orderId);
            if ($payment === null) {
                return null;
            }

            // جلوگیری از verify تکراری / race: فقط پرداخت pending پردازش می‌شود.
            if ($payment->getStatus() !== Payment::STATUS_PENDING) {
                return $payment;
            }

            $payment->setCallbackIp($clientIp);

            // چک امنیتی اجباری مستند: مقادیر بازگشتی باید با مقادیر مرحلهٔ Pay همین
            // پرداخت بخوانند (ضد parameter tampering). فقط وقتی درگاه این فیلدها را
            // برمی‌گرداند اعمال می‌شود (ملت: RefId + SaleOrderId؛ سپ آن‌ها را ندارد).
            $token = $payment->getGatewayToken();
            $refIdMismatch = $token !== null && isset($callbackData['RefId'])
                && !hash_equals($token, (string) $callbackData['RefId']);
            $orderMismatch = isset($callbackData['SaleOrderId'])
                && (string) $callbackData['SaleOrderId'] !== (string) $payment->getId();
            if ($refIdMismatch || $orderMismatch) {
                $payment->setStatus(Payment::STATUS_FAILED);
                $this->em->persist($payment);
                $this->log($payment, PaymentLog::ACTION_VERIFY, 'failed', null, $clientIp, ['reason' => 'tampering']);
                return $payment;
            }

            $gateway = $this->gateways->resolve($gatewayName);
            $result  = $gateway?->verify($callbackData);

            if ($result === null || !$result->success) {
                $canceled = $result !== null && $result->canceled;
                $payment->setStatus($canceled ? Payment::STATUS_CANCELED : Payment::STATUS_FAILED);
                $this->em->persist($payment);
                if (!$canceled) {
                    $this->circuitBreaker->recordFailure($gatewayName);
                }
                $this->log($payment, PaymentLog::ACTION_VERIFY, $payment->getStatus(), null, $clientIp, $this->sanitize($callbackData));
                return $payment;
            }

            $this->circuitBreaker->recordSuccess($gatewayName);

            // مبلغ تأییدشدهٔ درگاه باید با مبلغِ ثبت‌شده برابر باشد (ضد underpayment/دستکاری).
            if ($result->amountRials > 0 && $result->amountRials !== $payment->getAmountRials()) {
                $payment->setStatus(Payment::STATUS_FAILED);
                $this->em->persist($payment);
                $this->log($payment, PaymentLog::ACTION_VERIFY, 'failed', $result->referenceId, $clientIp, ['reason' => 'amount_mismatch']);
                return $payment;
            }

            // مرجع درگاه یکتاست؛ اگر متعلق به پرداخت دیگری باشد replay است.
            if ($result->referenceId !== '') {
                $owner = $this->paymentRepo->findByReferenceId($result->referenceId);
                if ($owner !== null && $owner->getId() !== $payment->getId()) {
                    $payment->setStatus(Payment::STATUS_FAILED);
                    $this->em->persist($payment);
                    $this->log($payment, PaymentLog::ACTION_VERIFY, 'failed', $result->referenceId, $clientIp, ['reason' => 'replay']);
                    return $payment;
                }
            }

            $payment->setStatus(Payment::STATUS_SUCCESS);
            $payment->setReferenceId($result->referenceId);
            // شمارهٔ کارت ماسک‌شدهٔ پرداخت‌کننده (ملت: CardHolderPan) برای نمایش در پنل.
            $cardPan = $callbackData['CardHolderPan'] ?? $callbackData['SecurePan'] ?? null;
            if ($cardPan) {
                $payment->setMetadata(($payment->getMetadata() ?? []) + ['card_pan' => (string) $cardPan]);
            }
            $this->em->persist($payment);

            $this->runPostAction($payment);

            $this->log($payment, PaymentLog::ACTION_VERIFY, 'success', $result->referenceId, $clientIp, $this->sanitize($callbackData));
            return $payment;
        });
    }

    /**
     * استرداد وجه (کل یا جزئی) یک پرداخت موفق. داخل transaction + قفل ردیف.
     * $amountRials = null → کل مبلغ. استرداد کامل → وضعیت refunded.
     */
    public function refundPayment(Payment $payment, ?int $amountRials, string $clientIp): PaymentRefundResult
    {
        return $this->em->wrapInTransaction(function () use ($payment, $amountRials, $clientIp): PaymentRefundResult {
            $locked = $this->paymentRepo->findByOrderIdForUpdate($payment->getOrderId());
            if ($locked === null) {
                return new PaymentRefundResult(false, errorMessage: 'پرداخت یافت نشد');
            }
            if ($locked->getStatus() !== Payment::STATUS_SUCCESS) {
                return new PaymentRefundResult(false, errorMessage: 'فقط پرداخت موفق قابل استرداد است');
            }

            $total    = $locked->getAmountRials();
            $meta     = $locked->getMetadata() ?? [];
            $refunded = array_sum(array_column($meta['refunds'] ?? [], 'amount'));
            $amount   = $amountRials ?? ($total - $refunded); // null = باقی‌ماندهٔ قابل استرداد
            if ($amount <= 0 || $refunded + $amount > $total) {
                return new PaymentRefundResult(false, errorMessage: 'مبلغ استرداد نامعتبر است');
            }

            $gateway = $this->gateways->resolve($locked->getGateway());
            $result  = $gateway?->refund((string) $locked->getId(), (string) $locked->getReferenceId(), $amount)
                ?? new PaymentRefundResult(false, errorMessage: 'درگاه نامعتبر است');

            if ($result->success) {
                $meta['refunds'][] = ['amount' => $amount, 'ref' => $result->refundRefId, 'at' => time()];
                $locked->setMetadata($meta);
                // فقط استرداد کامل، post-action (نوبت/اشتراک/کیف‌پول) را معکوس می‌کند.
                if ($refunded + $amount >= $total) {
                    $locked->setStatus(Payment::STATUS_REFUNDED);
                    $this->runReversePostAction($locked);
                }
                $this->em->persist($locked);
            }
            $this->log($locked, PaymentLog::ACTION_REFUND, $result->success ? 'success' : 'failed',
                $result->refundRefId ?: null, $clientIp, ['amount' => $amount, 'error' => $result->errorMessage]);
            return $result;
        });
    }

    /** برگشت وجه یک پرداخت settle‌نشده. در موفقیت وضعیت refunded. داخل transaction + قفل. */
    public function reversePayment(Payment $payment, string $clientIp): PaymentRefundResult
    {
        return $this->em->wrapInTransaction(function () use ($payment, $clientIp): PaymentRefundResult {
            $locked = $this->paymentRepo->findByOrderIdForUpdate($payment->getOrderId());
            if ($locked === null) {
                return new PaymentRefundResult(false, errorMessage: 'پرداخت یافت نشد');
            }
            if ($locked->getStatus() !== Payment::STATUS_SUCCESS) {
                return new PaymentRefundResult(false, errorMessage: 'فقط پرداخت موفق قابل برگشت است');
            }

            $gateway = $this->gateways->resolve($locked->getGateway());
            $result  = $gateway?->reverse((string) $locked->getId(), (string) $locked->getReferenceId())
                ?? new PaymentRefundResult(false, errorMessage: 'درگاه نامعتبر است');

            if ($result->success) {
                $locked->setStatus(Payment::STATUS_REFUNDED);
                $this->runReversePostAction($locked);
                $this->em->persist($locked);
            }
            $this->log($locked, PaymentLog::ACTION_REVERSE, $result->success ? 'success' : 'failed',
                null, $clientIp, ['error' => $result->errorMessage]);
            return $result;
        });
    }

    public function callbackUrl(Payment $payment): string
    {
        $prefix = $payment->getType() === Payment::TYPE_SUBSCRIPTION
            ? '/api/v1/subscription-payment/callback/'
            : '/api/v1/payment/callback/';
        return $this->appBaseUrl . $prefix . $payment->getGateway() . '?order_id=' . $payment->getOrderId();
    }

    // ── Post-actions ──────────────────────────────────────────────────────────

    private function runPostAction(Payment $payment): void
    {
        match ($payment->getType()) {
            Payment::TYPE_SUBSCRIPTION => $this->handleSubscriptionActivation($payment),
            Payment::TYPE_SMS_WALLET   => $this->handleSmsWalletCharge($payment),
            Payment::TYPE_APPOINTMENT  => $this->handleAppointmentConfirmation($payment),
            default                    => null,
        };
    }

    /** معکوس‌سازی اثر پرداخت هنگام استرداد کامل / برگشت وجه. */
    private function runReversePostAction(Payment $payment): void
    {
        match ($payment->getType()) {
            Payment::TYPE_SUBSCRIPTION => $this->subscriptionService->deleteByPayment($payment),
            Payment::TYPE_SMS_WALLET   => $this->reverseSmsWalletCharge($payment),
            Payment::TYPE_APPOINTMENT  => $this->reverseAppointment($payment),
            default                    => null,
        };
    }

    private function reverseAppointment(Payment $payment): void
    {
        $appointment = $payment->getAppointment();
        if ($appointment === null || !$appointment->canTransitionTo(Appointment::STATUS_CANCELLED_BY_USER)) {
            return;
        }
        // لغو نوبت → اسلات به‌طور خودکار آزاد می‌شود (availability از نوبت‌های active محاسبه می‌شود).
        $appointment->transitionTo(Appointment::STATUS_CANCELLED_BY_USER);
        $this->em->persist($appointment);
    }

    private function reverseSmsWalletCharge(Payment $payment): void
    {
        $meta       = $payment->getMetadata() ?? [];
        $entityType = $meta['entity_type'] ?? null;
        $entityId   = isset($meta['entity_id']) ? (int) $meta['entity_id'] : null;
        if ($entityType === null || $entityId === null) {
            return;
        }
        $wallet = $this->smsWalletService->getOrCreate($entityType, $entityId);
        $this->smsWalletService->deduct($wallet, $payment->getAmountRials(), 'استرداد شارژ کیف پیامک');
    }

    private function handleAppointmentConfirmation(Payment $payment): void
    {
        $appointment = $payment->getAppointment();
        if ($appointment === null || !$appointment->canTransitionTo(Appointment::STATUS_CONFIRMED)) {
            return;
        }

        $appointment->transitionTo(Appointment::STATUS_CONFIRMED);
        $this->em->persist($appointment);

        $doctor = $appointment->getDoctor();
        $this->commissionService->processAppointment(
            $payment,
            $doctor->getRepresentationId(),
            $this->bookingRepresentationIdFor($payment),
            $doctor->getId(),
        );

        $mobile = $appointment->getPatientMobile();
        if ($mobile) {
            $this->smsService->dispatchTemplate(SmsLog::TAG_PAYMENT, $mobile, [
                'doctor' => $doctor->getName(),
                'date'   => $this->jalali->formatDateTime($appointment->getSlotStart(), false),
                'time'   => date('H:i', $appointment->getSlotStart()),
            ]);
        }

        // اعلان به شمارهٔ «اعلان نوبت» دکتر — فقط برای نوبت‌های پرداخت‌شدهٔ سایت (همین مسیر).
        $notify = $doctor->getNotificationMobile();
        if ($notify) {
            $this->smsService->dispatchTemplate(SmsLog::TAG_DOCTOR_APPOINTMENT, $notify, [
                'patient' => $appointment->getPatientName() ?? '—',
                'date'    => $this->jalali->formatDateTime($appointment->getSlotStart(), false),
                'time'    => date('H:i', $appointment->getSlotStart()),
            ]);
        }
    }

    private function handleSubscriptionActivation(Payment $payment): void
    {
        $periodUuid = ($payment->getMetadata() ?? [])['period_uuid'] ?? null;
        if ($periodUuid === null) {
            return;
        }

        $user   = $payment->getUser();
        $doctor = $this->doctorRepo->findByUser($user);
        $bookingRepId = $this->bookingRepresentationIdFor($payment);

        if ($doctor !== null) {
            $this->subscriptionService->createFromPayment($payment, 'doctor', $doctor->getId(), $periodUuid);
            $this->commissionService->processSubscription($payment, $doctor->getRepresentationId(), $bookingRepId, $doctor->getId(), null);
            return;
        }

        $clinic = $this->clinicRepo->findByUser($user);
        if ($clinic !== null) {
            $this->subscriptionService->createFromPayment($payment, 'clinic', $clinic->getId(), $periodUuid);
            $this->commissionService->processSubscription($payment, $clinic->getRepresentationId(), $bookingRepId, null, $clinic->getId());
        }
    }

    private function handleSmsWalletCharge(Payment $payment): void
    {
        $meta       = $payment->getMetadata() ?? [];
        $entityType = $meta['entity_type'] ?? null;
        $entityId   = isset($meta['entity_id']) ? (int) $meta['entity_id'] : null;
        if ($entityType === null || $entityId === null) {
            return;
        }

        $wallet = $this->smsWalletService->getOrCreate($entityType, $entityId);
        $this->smsWalletService->charge($wallet, $payment->getAmountRials(), $payment);
    }

    // ── Helpers ───────────────────────────────────────────────────────────────

    private function failPayment(Payment $payment, string $action, array $payload): void
    {
        $payment->setStatus(Payment::STATUS_FAILED);
        $this->paymentRepo->save($payment);
        $this->log($payment, $action, 'failed', null, null, $payload);
    }

    private function log(Payment $payment, string $action, string $result, ?string $authority, ?string $clientIp, ?array $payload): void
    {
        try {
            $this->paymentLogRepo->save(new PaymentLog(
                (int) $payment->getId(),
                $action,
                $payment->getGateway(),
                $result,
                $authority,
                $clientIp,
                $payload,
            ));
        } catch (\Throwable $e) {
            // لاگ نباید جریان پرداخت را بشکند.
            $this->logger->error('PaymentLog write failed: ' . $e->getMessage(), ['orderId' => $payment->getOrderId()]);
        }
    }

    /** حذف کلیدهای حساس احتمالی از payload کال‌بک قبل از ذخیره. */
    private function sanitize(array $data): array
    {
        unset($data['password'], $data['userPassword'], $data['userName']);
        return $data;
    }
}
