<?php

namespace App\Payment\Gateway;

use App\Config\Repository\SiteConfigRepository;
use Psr\Log\LoggerInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface;

class MellatGateway implements PaymentGatewayInterface
{
    private const PAYMENT_URL = 'https://bpm.shaparak.ir/pgwchannel/startpay.mellat';
    // WSDL عملیاتی؛ prod با SoapClient بومی روی این WSDL کار می‌کند (طبق آموزش رسمی v1.38).
    private const PROD_WSDL   = 'https://bpm.shaparak.ir/pgwchannel/services/pgw?wsdl';

    // sandbox banktest.ir (موقت). پشت فلگ mellat_sandbox.
    // نکته: SOAP sandbox (pgwchannel) روی banktest 502 می‌دهد؛ فقط REST (ipg2) سالم است،
    // پس در حالت sandbox از REST (JSON + Basic Auth) استفاده می‌شود، نه SOAP.
    // نکته: API روی REST (ipg2) است ولی فرمِ پرداخت فقط روی pgwchannel/startpay.mellat سالم است (ipg2/startpay = 404).
    private const SANDBOX_REST_BASE   = 'https://sandbox.banktest.ir/mellat/bpm.shaparak.ir/ipg2/rest';
    private const SANDBOX_PAYMENT_URL = 'https://sandbox.banktest.ir/mellat/bpm.shaparak.ir/pgwchannel/startpay.mellat';
    private const SANDBOX_TERMINAL_ID = '134759344';
    private const SANDBOX_USERNAME    = 'user134759344';
    private const SANDBOX_PASSWORD    = '17384843';

    private ?\SoapClient $soap = null;

    public function __construct(
        private readonly HttpClientInterface   $httpClient,
        private readonly SiteConfigRepository $configRepo,
        private readonly LoggerInterface      $logger,
        private readonly ?string $terminalId = '',
        private readonly ?string $username   = '',
        private readonly ?string $password   = '',
    ) {}

    public function getName(): string
    {
        return 'mellat';
    }

    public function isConfigured(): bool
    {
        return $this->cfg('mellat_terminal_id', $this->terminalId) !== ''
            && $this->cfg('mellat_username',    $this->username)   !== ''
            && $this->cfg('mellat_password',    $this->password)   !== '';
    }

    private function sandbox(): bool
    {
        return $this->configRepo->get('mellat_sandbox') === '1';
    }

    private function paymentUrl(): string
    {
        return $this->sandbox() ? self::SANDBOX_PAYMENT_URL : self::PAYMENT_URL;
    }

    // ── prod SOAP ───────────────────────────────────────────────────────────────

    /** WSDL قابل override با کلید تنظیم mellat_wsdl_url (برای WSDL تستِ pgw.dev اگر لازم شد). */
    private function wsdlUrl(): string
    {
        return (string) ($this->configRepo->get('mellat_wsdl_url') ?: self::PROD_WSDL);
    }

    /** SoapClient تنبل — فقط prod و فقط وقتی لازم شد ساخته می‌شود. */
    private function soap(): \SoapClient
    {
        if (!class_exists(\SoapClient::class)) {
            throw new \App\Shared\Exception\AppException(\App\Shared\Constant\ErrorCodes::ERR_PAYMENT_001, null, 503);
        }
        if ($this->soap === null) {
            $this->soap = new \SoapClient($this->wsdlUrl(), [
                'trace'              => true,
                'exceptions'         => true,
                'encoding'           => 'UTF-8',
                'connection_timeout' => 10,
            ]);
        }
        return $this->soap;
    }

    /** پارامترهای مشترک احراز هویت prod. */
    private function soapAuth(): array
    {
        return [
            'terminalId'   => (int) $this->cfg('mellat_terminal_id', $this->terminalId),
            'userName'     => $this->cfg('mellat_username', $this->username),
            'userPassword' => $this->cfg('mellat_password', $this->password),
        ];
    }

    // ── sandbox REST ─────────────────────────────────────────────────────────────

    /** هدر Basic Auth برای REST sandbox (base64 از userName:userPassword). */
    private function restHeaders(): array
    {
        $auth = base64_encode(self::SANDBOX_USERNAME . ':' . self::SANDBOX_PASSWORD);
        return ['Content-Type' => 'application/json', 'Authorization' => 'Basic ' . $auth];
    }

    /** پاسخ REST یک رشتهٔ ساده مثل `0,RefId` یا `0` است (گاهی داخل "…"). */
    private function restParts(string $body): array
    {
        $body = trim($body, " \t\n\r\0\x0B\"");
        return array_map('trim', explode(',', $body));
    }

    /** یک فراخوانی REST sandbox؛ فقط کد پاسخ (بخش اول) را برمی‌گرداند. */
    private function restCall(string $path, array $json): string
    {
        $body = $this->httpClient->request('POST', self::SANDBOX_REST_BASE . $path, [
            'json'    => $json,
            'headers' => $this->restHeaders(),
            'timeout' => 10,
        ])->getContent();

        return $this->restParts($body)[0] ?? '-1';
    }

    private function cfg(string $key, ?string $envFallback): string
    {
        // در sandbox، credentials از config خوانده نمی‌شوند تا مقادیر واقعیِ prod نشتی نکنند.
        if ($this->sandbox()) {
            return match ($key) {
                'mellat_terminal_id' => self::SANDBOX_TERMINAL_ID,
                'mellat_username'    => self::SANDBOX_USERNAME,
                'mellat_password'    => self::SANDBOX_PASSWORD,
                default              => $envFallback ?? '',
            };
        }
        return (string) ($this->configRepo->get($key) ?: $envFallback ?? '');
    }

    public function initiate(int $amountRials, string $orderId, string $callbackUrl): PaymentInitResult
    {
        try {
            if ($this->sandbox()) {
                [$resCode, $refId] = $this->restParts(
                    $this->httpClient->request('POST', self::SANDBOX_REST_BASE . '/bpPayRequest', [
                        'json' => [
                            'terminalId'     => (int) self::SANDBOX_TERMINAL_ID,
                            'userName'       => self::SANDBOX_USERNAME,
                            'userPassword'   => self::SANDBOX_PASSWORD,
                            'orderId'        => (int) $orderId,
                            'amount'         => $amountRials,
                            'localDate'      => $this->date(),
                            'localTime'      => $this->time(),
                            'additionalData' => '',
                            'callBackUrl'    => $callbackUrl,
                            'payerId'        => '0',
                        ],
                        'headers' => $this->restHeaders(),
                        'timeout' => 10,
                    ])->getContent()
                ) + ['-1', ''];
            } else {
                $r = $this->soap()->bpPayRequest($this->soapAuth() + [
                    'orderId'        => (int) $orderId,
                    'amount'         => $amountRials,
                    'localDate'      => $this->date(),
                    'localTime'      => $this->time(),
                    'additionalData' => '',
                    'callBackUrl'    => $callbackUrl,
                    'payerId'        => 0,
                ]);
                // پاسخ "resCode,refId"
                $parts   = array_map('trim', explode(',', (string) ($r->return ?? ''), 2));
                $resCode = $parts[0] ?? '-1';
                $refId   = $parts[1] ?? '';
            }

            if ($resCode !== '0') {
                return new PaymentInitResult(false, errorMessage: $this->mellatMessage($resCode));
            }

            $redirectUrl = $this->paymentUrl() . '?RefId=' . $refId;

            // درگاه ملت باید با POST فرم (فیلد RefId) باز شود؛ redirectUrl (شامل RefId)
            // برای سازگاری با مصرف‌کننده‌های قدیمی نگه داشته می‌شود.
            return new PaymentInitResult(
                true,
                redirectUrl: $redirectUrl,
                token: $refId,
                redirectMethod: 'POST',
                redirectParams: ['RefId' => $refId],
            );
        } catch (\Throwable $e) {
            $this->logger->error(sprintf('Payment initiate failed (mellat): %s @ %s:%d', $e->getMessage(), $e->getFile(), $e->getLine()), ['exception' => $e, 'orderId' => $orderId, 'amount' => $amountRials]);
            return new PaymentInitResult(false, errorMessage: $e->getMessage());
        }
    }

    public function verify(array $callbackData): PaymentVerifyResult
    {
        $resCode         = $callbackData['ResCode']         ?? '';
        $saleOrderId     = $callbackData['SaleOrderId']     ?? '';
        $saleReferenceId = $callbackData['SaleReferenceId'] ?? '';

        if ($resCode === '17') {
            return new PaymentVerifyResult(false, errorMessage: 'پرداخت توسط کاربر لغو شد', canceled: true);
        }

        if ($resCode !== '0') {
            return new PaymentVerifyResult(false, errorMessage: $this->mellatMessage($resCode));
        }

        // برای تأیید و واریز، ملت به saleOrderId (همان orderId مرحلهٔ Pay) و
        // saleReferenceId (که در callback برمی‌گردد) نیاز دارد — نه RefId.
        if ($saleOrderId === '' || $saleReferenceId === '') {
            return new PaymentVerifyResult(false, errorMessage: 'اطلاعات بازگشتی درگاه ناقص است');
        }

        try {
            // هم sandbox و هم prod: verify سپس settle جدا. 0=موفق، 43=قبلاً verify، 45=قبلاً settle.
            if ($this->sandbox()) {
                $payload = [
                    'terminalId'      => (int) self::SANDBOX_TERMINAL_ID,
                    'userName'        => self::SANDBOX_USERNAME,
                    'userPassword'    => self::SANDBOX_PASSWORD,
                    'orderId'         => (int) $saleOrderId,
                    'saleOrderId'     => (int) $saleOrderId,
                    'saleReferenceId' => (int) $saleReferenceId,
                ];
                $vc = $this->restCall('/bpVerifyRequest', $payload);
                if (!in_array($vc, ['0', '43'], true)) {
                    return new PaymentVerifyResult(false, errorMessage: $this->mellatMessage($vc));
                }
                $sc = $this->restCall('/bpSettleRequest', $payload);
                if (!in_array($sc, ['0', '45'], true)) {
                    return new PaymentVerifyResult(false, errorMessage: $this->mellatMessage($sc));
                }
                return new PaymentVerifyResult(true, referenceId: $saleReferenceId);
            }

            $auth = $this->soapAuth() + [
                'orderId'         => (int) $saleOrderId,
                'saleOrderId'     => (int) $saleOrderId,
                'saleReferenceId' => (int) $saleReferenceId,
            ];
            $vc = (string) ($this->soap()->bpVerifyRequest($auth)->return ?? '-1');
            if (!in_array($vc, ['0', '43'], true)) {
                return new PaymentVerifyResult(false, errorMessage: $this->mellatMessage($vc));
            }
            $sc = (string) ($this->soap()->bpSettleRequest($auth)->return ?? '-1');
            if (!in_array($sc, ['0', '45'], true)) {
                return new PaymentVerifyResult(false, errorMessage: $this->mellatMessage($sc));
            }
            return new PaymentVerifyResult(true, referenceId: $saleReferenceId);
        } catch (\Throwable $e) {
            $this->logger->error(sprintf('Payment verify failed (mellat): %s @ %s:%d', $e->getMessage(), $e->getFile(), $e->getLine()), ['exception' => $e, 'saleReferenceId' => $saleReferenceId]);
            return new PaymentVerifyResult(false, errorMessage: $e->getMessage());
        }
    }

    public function refund(string $saleOrderId, string $saleReferenceId, int $refundAmountRials): PaymentRefundResult
    {
        try {
            if ($this->sandbox()) {
                $parts = $this->restParts(
                    $this->httpClient->request('POST', self::SANDBOX_REST_BASE . '/bpRefundRequest', [
                        'json'    => $this->refundPayload($saleOrderId, $saleReferenceId, $refundAmountRials),
                        'headers' => $this->restHeaders(),
                        'timeout' => 10,
                    ])->getContent()
                );
            } else {
                $r = $this->soap()->bpRefundRequest($this->soapAuth() + [
                    'orderId'         => $this->uniqueOrderId(),
                    'saleOrderId'     => (int) $saleOrderId,
                    'saleReferenceId' => (int) $saleReferenceId,
                    'refundAmount'    => $refundAmountRials,
                ]);
                $parts = array_map('trim', explode(',', (string) ($r->return ?? ''), 2));
            }
            $code = $parts[0] ?? '-1';
            if ($code !== '0') {
                return new PaymentRefundResult(false, errorMessage: $this->mellatMessage($code));
            }
            return new PaymentRefundResult(true, refundRefId: $parts[1] ?? '');
        } catch (\Throwable $e) {
            $this->logger->error('Payment refund failed (mellat): ' . $e->getMessage(), ['saleReferenceId' => $saleReferenceId]);
            return new PaymentRefundResult(false, errorMessage: $e->getMessage());
        }
    }

    public function reverse(string $saleOrderId, string $saleReferenceId): PaymentRefundResult
    {
        try {
            if ($this->sandbox()) {
                $code = $this->restCall('/bpReversalRequest', $this->reversalPayload($saleOrderId, $saleReferenceId));
            } else {
                $code = (string) ($this->soap()->bpReversalRequest($this->soapAuth() + [
                    'orderId'         => $this->uniqueOrderId(),
                    'saleOrderId'     => (int) $saleOrderId,
                    'saleReferenceId' => (int) $saleReferenceId,
                ])->return ?? '-1');
            }
            // 0 = موفق، 48 = پیشتر reverse شده (idempotent = موفق).
            if (!in_array($code, ['0', '48'], true)) {
                return new PaymentRefundResult(false, errorMessage: $this->mellatMessage($code));
            }
            return new PaymentRefundResult(true);
        } catch (\Throwable $e) {
            $this->logger->error('Payment reversal failed (mellat): ' . $e->getMessage(), ['saleReferenceId' => $saleReferenceId]);
            return new PaymentRefundResult(false, errorMessage: $e->getMessage());
        }
    }

    /** نگاشت کد پاسخ ملت به پیام فارسی (جدول مستند ۱.۳۸). */
    private function mellatMessage(string $code): string
    {
        $map = [
            '11' => 'شماره کارت نامعتبر است',
            '12' => 'موجودی کافی نیست',
            '17' => 'کاربر از انجام تراکنش منصرف شده است',
            '19' => 'مبلغ استرداد بیش از مبلغ تراکنش خرید است',
            '21' => 'پذیرنده نامعتبر است',
            '24' => 'اطلاعات کاربری پذیرنده نامعتبر است',
            '25' => 'مبلغ نامعتبر است',
            '34' => 'خطای سیستمی درگاه (در محیط تست، استرداد پشتیبانی نمی‌شود)',
            '41' => 'شماره درخواست تکراری است',
            '42' => 'تراکنش خرید (Sale) یافت نشد',
            '43' => 'این تراکنش پیشتر تأیید شده است',
            '44' => 'درخواست تأیید یافت نشد',
            '45' => 'این تراکنش پیشتر واریز (Settle) شده است',
            '46' => 'تراکنش واریز (Settle) نشده است',
            '47' => 'تراکنش واریز یافت نشد',
            '48' => 'این تراکنش پیشتر برگشت (Reverse) شده است',
            '51' => 'تراکنش تکراری است',
            '61' => 'خطا در واریز',
            '62' => 'مسیر بازگشت در دامنهٔ ثبت‌شدهٔ پذیرنده نیست',
            '421' => 'IP نامعتبر است (به بانک اعلام نشده)',
        ];
        return ($map[$code] ?? 'خطای درگاه') . " (کد $code)";
    }

    /** orderId یکتای عددی برای هر درخواست refund/reverse (مستند: هر بار باید یکتا باشد). */
    private function uniqueOrderId(): int
    {
        return (int) substr((string) (int) (microtime(true) * 1000), -12);
    }

    /** بدنهٔ JSON مشترک refund (REST sandbox). */
    private function refundPayload(string $saleOrderId, string $saleReferenceId, int $refundAmountRials): array
    {
        return $this->reversalPayload($saleOrderId, $saleReferenceId) + ['refundAmount' => $refundAmountRials];
    }

    /** بدنهٔ JSON مشترک reverse (REST sandbox). */
    private function reversalPayload(string $saleOrderId, string $saleReferenceId): array
    {
        return [
            'terminalId'      => (int) $this->cfg('mellat_terminal_id', $this->terminalId),
            'userName'        => $this->cfg('mellat_username', $this->username),
            'userPassword'    => $this->cfg('mellat_password', $this->password),
            'orderId'         => $this->uniqueOrderId(),
            'saleOrderId'     => (int) $saleOrderId,
            'saleReferenceId' => (int) $saleReferenceId,
        ];
    }

    private function date(): string
    {
        return date('Ymd');
    }
    private function time(): string
    {
        return date('His');
    }
}
