<?php

namespace App\Modules\Payment\Yapikredi;

use App\Core\Payment\AbstractPayment;

/**
 * Yapı Kredi Posnet (OOS 3D-Secure) sanal POS.
 *
 * Referans: mews/pos PosNet.php + PosNetCrypt.php (WiseCP vendor) + resmî Posnet OOS akışı.
 * Kritik sözleşmeler:
 *  - XML encoding ISO-8859-9 (PosNet.php:134).
 *  - MAC iki-geçiş: firstHash=base64(sha256(storeKey;terminalId)); mac=base64(sha256(orderId;amount;currency;clientId;firstHash)) (PosNet.php:327-338).
 *  - 3DS dönüşü check3DHash (3DES-EDE3-CBC decrypt + strict `===`, PosNet.php:626-653) + verifyResponseMAC (PosNet.php:347-358) fail-CLOSED.
 *  - orderID sıfır-pad 20 (PosNet.php:368-375); status/refund için TDSC-prefixed 24 (PosNet.php:401-411).
 *
 * Config: merchant_id=mid/clientId, terminal_id=tid, posnet_id, enc_key=storeKey.
 */
class Yapikredi extends AbstractPayment
{
    private const ORDER_ID_LENGTH = 20;

    private const ORDER_ID_TOTAL_LENGTH = 24;

    private const ORDER_ID_3D_PREFIX = 'TDSC';

    protected $apiUrl;

    protected $threeDGatewayUrl;

    public function __construct(array $config = [])
    {
        parent::__construct($config);

        $this->apiUrl = $this->getApiUrl();
        $this->threeDGatewayUrl = $this->get3DGatewayUrl();
    }

    /**
     * Odeme girisimi baslatir - kart ve tutar dogrulamasi yapar (ag cagrisi yok).
     *
     * @return array
     */
    public function attemptPayment(array $params)
    {
        $requiredParams = ['amount', 'card_number', 'exp_month', 'exp_year', 'cvv'];

        foreach ($requiredParams as $param) {
            if (! isset($params[$param]) || empty($params[$param])) {
                return [
                    'status' => 'error',
                    'message' => __('admin/modules.yapikredi_missing_param', ['param' => $param]),
                ];
            }
        }

        if ($params['amount'] <= 0) {
            return [
                'status' => 'error',
                'message' => __('admin/modules.yapikredi_invalid_amount'),
            ];
        }

        $cardNumber = preg_replace('/\s+/', '', $params['card_number']);
        if (! preg_match('/^\d{13,19}$/', $cardNumber)) {
            return [
                'status' => 'error',
                'message' => __('admin/modules.yapikredi_invalid_card'),
            ];
        }

        if (! $this->isValidLuhn($cardNumber)) {
            return [
                'status' => 'error',
                'message' => __('admin/modules.yapikredi_invalid_card_number'),
            ];
        }

        $fraudCheck = $this->performPreFraudCheck($params);
        if ($fraudCheck['status'] === 'error') {
            return $fraudCheck;
        }

        return [
            'status' => 'ready',
            'message' => __('admin/modules.yapikredi_ready'),
            'data' => [
                'payment_method' => 'yapikredi',
                'amount' => $params['amount'],
                'currency' => $params['currency'] ?? 'TRY',
                'order_id' => $params['order_id'] ?? null,
            ],
        ];
    }

    /**
     * 2D odeme (Posnet 'sale' XML). Referans createRegularPaymentXML (PosNet.php:433-455).
     *
     * @return array
     */
    public function makePayment(array $params)
    {
        $orderId = $this->formatOrderId($params['order_id'] ?? ('YKB' . time()));
        $amount = $this->amountMinor($params['amount']);
        $cardNumber = str_replace([' ', '-'], '', $params['card_number']);
        $expDate = $this->formatExpDate($params); // YYMM
        $cvv = (string) $params['cvv'];
        $currencyCode = $this->posnetCurrency($params['currency'] ?? null); // ISO → POSnet formu (TRY→TL)
        $installment = $this->formatInstallment($params['installment'] ?? 0);

        // Referans createRegularPaymentXML alan sirasi + tranDateRequired (PosNet.php:438).
        $body = [
            'sale' => [
                'orderID' => $orderId,
                'installment' => $installment,
                'amount' => $amount,
                'currencyCode' => $currencyCode,
                'ccno' => $cardNumber,
                'expDate' => $expDate,
                'cvc' => $cvv,
            ],
        ];

        $xml = $this->buildPosnetXml($body, true);
        $parsed = $this->send($xml, $orderId);

        return $this->mapPaymentResponse($parsed, $orderId);
    }

    /**
     * 3D Secure baslatir - oosRequestData ile data1/data2/sign alir + 3D form dondurur.
     * Referans getOosTransactionData + get3DFormData (PosNet.php:154-263).
     *
     * @return array
     */
    public function start3DPayment(array $params)
    {
        $merchantId = (string) $this->getConfig('merchant_id');
        $posnetId = (string) $this->getConfig('posnet_id');

        $orderId = $this->formatOrderId($params['order_id'] ?? ('YKB' . time()));
        $amount = $this->amountMinor($params['amount']);
        $cardNumber = str_replace([' ', '-'], '', $params['card_number']);
        $expDate = $this->formatExpDate($params); // YYMM
        $cvv = (string) $params['cvv'];
        $currencyCode = $this->posnetCurrency($params['currency'] ?? null); // ISO → POSnet formu (TRY→TL)
        $installment = $this->formatInstallment($params['installment'] ?? 0);

        // Referans op = oosRequestData (PosNet.php:163).
        $body = [
            'oosRequestData' => [
                'posnetid' => $posnetId,
                'ccno' => $cardNumber,
                'expDate' => $expDate,
                'cvc' => $cvv,
                'amount' => $amount,
                'currencyCode' => $currencyCode,
                'installment' => $installment,
                'XID' => $orderId,
                'cardHolderName' => $params['card_holder'] ?? ($params['name'] ?? ''),
                'tranType' => 'Sale',
            ],
        ];

        $xml = $this->buildPosnetXml($body, false);
        $parsed = $this->send($xml, $orderId);

        if (! $parsed || ($parsed['approved'] ?? '0') !== '1') {
            return [
                'status' => 'error',
                'message' => $parsed['respText'] ?? __('admin/modules.yapikredi_3d_init_failed'),
                'error_code' => $parsed['respCode'] ?? '',
                'data' => $parsed,
            ];
        }

        // Referans oosRequestDataResponse.{data1,data2,sign} (PosNet.php:244-248).
        $data1 = $parsed['data1'] ?? '';
        $data2 = $parsed['data2'] ?? '';
        $sign = $parsed['sign'] ?? '';

        if ($data1 === '' || $data2 === '') {
            return [
                'status' => 'error',
                'message' => __('admin/modules.yapikredi_3d_init_failed'),
                'data' => $parsed,
            ];
        }

        $successUrl = $params['success_url'] ?? '';

        // Referans 3D form inputs (PosNet.php:243-253). MAC form'a KONMAZ; banka data1/data2'yi cozer.
        $formData = [
            'posnetData' => $data1,
            'posnetData2' => $data2,
            'mid' => $merchantId,
            'posnetID' => $posnetId,
            'digest' => $sign,
            'vftCode' => (string) $this->getConfig('promotion_code', ''),
            'merchantReturnURL' => $successUrl,
            'url' => '',
            'lang' => $params['lang'] ?? 'tr',
        ];

        return [
            'status' => '3d_redirect',
            'message' => __('admin/modules.yapikredi_3d_redirect'),
            'redirect_url' => $this->threeDGatewayUrl,
            'method' => 'POST',
            'form_data' => $formData,
            'data' => [
                'order_id' => $orderId,
                'amount' => $amount,
            ],
        ];
    }

    /**
     * 3D Secure tamamlar. Referans make3DPayment (PosNet.php:186-217):
     *  1) check3DHash (3DES decrypt + strict compare) FAIL-CLOSED,
     *  2) oosResolveMerchantData (mac dahil),
     *  3) verifyResponseMAC FAIL-CLOSED,
     *  4) mdStatus in {1,2,3,4} → create3DPaymentXML (bankData+merchantData+sign+wpAmount+mac).
     *
     * @return array
     */
    public function complete3DPayment(array $params)
    {
        $merchantId = (string) $this->getConfig('merchant_id');
        $storeKey = $this->storeKey();

        $merchantPacket = (string) ($params['MerchantPacket'] ?? $params['merchantPacket'] ?? '');
        $bankPacket = (string) ($params['BankPacket'] ?? $params['bankPacket'] ?? '');
        $sign = (string) ($params['Sign'] ?? $params['sign'] ?? '');

        if ($merchantPacket === '' || $bankPacket === '' || $sign === '') {
            return [
                'status' => 'error',
                'message' => __('admin/modules.yapikredi_3d_missing_data'),
            ];
        }

        // 3DS return'un bize ait siparise ait oldugunu bilmek icin orijinal siparis konteksti gerekir.
        $orderId = $this->formatOrderId(
            $params['order_id'] ?? ($params['xid'] ?? ($params['XID'] ?? ''))
        );
        $amount = $this->amountMinor($params['amount'] ?? null, isset($params['amount_minor']) ? $params['amount_minor'] : null);
        // Caller (PRC-1058) callback'e currency=ISO (TRY/USD/EUR) enjekte eder; ancak MAC/create3DHash
        // banka ile AYNI POSnet-form birimi (TL/US/EU) üzerinden hesaplanmalı, yoksa fail-closed doğrulama
        // geçerli 3DS dönüşlerini yanlış reddeder (referans MAC currency = request'teki POSnet-form).
        $currencyCode = $this->posnetCurrency($params['currency'] ?? null);
        $installment = $this->formatInstallment($params['installment'] ?? 0);

        // (1) FAIL-CLOSED: MerchantPacket'i StoreKey ile decrypt et + katı karşılaştır (PosNet.php:626-653).
        if (! $this->check3DHash($merchantPacket, $storeKey, $merchantId, $orderId, $amount, $installment)) {
            return [
                'status' => 'error',
                'message' => __('admin/modules.yapikredi_3d_hash_invalid'),
            ];
        }

        // (2) oosResolveMerchantData — mac DAHİL (PosNet.php:502-516).
        $resolveBody = [
            'oosResolveMerchantData' => [
                'bankData' => $bankPacket,
                'merchantData' => $merchantPacket,
                'sign' => $sign,
                'mac' => $this->create3DHash($orderId, $amount, $currencyCode, $merchantId, $storeKey),
            ],
        ];
        $resolveXml = $this->buildPosnetXml($resolveBody, false);
        $resolved = $this->send($resolveXml, $orderId);

        if (! $resolved || ($resolved['approved'] ?? '0') !== '1') {
            return [
                'status' => 'error',
                'message' => $resolved['respText'] ?? __('admin/modules.yapikredi_3d_resolve_failed'),
                'error_code' => $resolved['respCode'] ?? '',
                'data' => $resolved,
            ];
        }

        // (3) FAIL-CLOSED: banka MAC'ini secureCompare ile doğrula (PosNet.php:347-358).
        $mdStatus = $resolved['mdStatus'] ?? '0';
        $expectedMac = $this->verifyResponseMac($mdStatus, $orderId, $amount, $currencyCode, $merchantId, $storeKey);
        if (! $this->secureCompare($expectedMac, $resolved['mac'] ?? null)) {
            return [
                'status' => 'error',
                'message' => __('admin/modules.yapikredi_3d_mac_invalid'),
            ];
        }

        // (4) 3D dogrulama sonucu (PosNet.php:207).
        if (! in_array((string) $mdStatus, ['1', '2', '3', '4'], true)) {
            return [
                'status' => 'error',
                'message' => __('admin/modules.yapikredi_3d_auth_failed'),
                'data' => [
                    'mdStatus' => $mdStatus,
                    'mdErrorMessage' => $resolved['mdErrorMessage'] ?? '',
                ],
            ];
        }

        // Referans create3DPaymentXML: bankData + merchantData + sign + wpAmount + mac (PosNet.php:480-495).
        $tranBody = [
            'oosTranData' => [
                'bankData' => $bankPacket,
                'merchantData' => $merchantPacket,
                'sign' => $sign,
                'wpAmount' => 0,
                'mac' => $this->create3DHash($orderId, $amount, $currencyCode, $merchantId, $storeKey),
            ],
        ];
        $tranXml = $this->buildPosnetXml($tranBody, false);
        $tran = $this->send($tranXml, $orderId);

        // FAIL-CLOSED (doküman §4, 2.0.1.4): finalize cevabının banka'dan geldiğini teyit et —
        // response mac = HASH(hostLogkey;xid;amount;currency;merchantNo;firstHash). Sunucu-sunucu
        // TLS cevabı olduğundan mac YOKSA bloklamaz (verify-if-present); mac VARSA yanlışsa reddet.
        if (is_array($tran) && ! empty($tran['mac'])) {
            $expectedFinalizeMac = $this->finalizeResponseMac(
                (string) ($tran['hostlogkey'] ?? ''),
                $orderId,
                $amount,
                $currencyCode,
                $merchantId,
                $storeKey
            );
            if (! $this->secureCompare($expectedFinalizeMac, (string) $tran['mac'])) {
                return [
                    'status' => 'error',
                    'message' => __('admin/modules.yapikredi_3d_finalize_mac_invalid'),
                ];
            }
        }

        $result = $this->mapPaymentResponse($tran, $orderId);
        if ($result['status'] === 'success') {
            $result['data'] = array_merge($result['data'] ?? [], ['mdStatus' => $mdStatus]);
        }

        return $result;
    }

    /**
     * 3D Secure POST donusunu isler — imzayi complete3DPayment icinde FAIL-CLOSED dogrular.
     *
     * @return array
     */
    public function handlePaymentCallback(array $params)
    {
        // 3D Secure donusu: MerchantPacket/BankPacket/Sign. Boslugu burada eleriz;
        // KRIPTOGRAFIK dogrulama (check3DHash + verifyResponseMAC) complete3DPayment icinde FAIL-CLOSED.
        $hasPacket = isset($params['MerchantPacket']) || isset($params['merchantPacket']);
        if ($hasPacket) {
            $merchantPacket = (string) ($params['MerchantPacket'] ?? $params['merchantPacket'] ?? '');
            $bankPacket = (string) ($params['BankPacket'] ?? $params['bankPacket'] ?? '');
            $sign = (string) ($params['Sign'] ?? $params['sign'] ?? '');

            if ($merchantPacket === '' || $bankPacket === '' || $sign === '') {
                return [
                    'status' => 'error',
                    'message' => __('admin/modules.yapikredi_invalid_callback'),
                ];
            }

            return $this->complete3DPayment($params);
        }

        return [
            'status' => 'error',
            'message' => __('admin/modules.yapikredi_invalid_callback'),
        ];
    }

    /**
     * Iade (Posnet 'return' XML). Referans createRefundXML (PosNet.php:575-594):
     * tranDateRequired=1 + hostLogKey tercihli (yoksa TDSC-prefixed orderID).
     *
     * @return array
     */
    public function refundPayment(array $params)
    {
        $amount = $this->amountMinor($params['amount']);
        $currencyCode = $this->getConfig('currency_code', 'TL');
        $hostLogKey = $params['host_ref_num'] ?? ($params['hostlogkey'] ?? ($params['transaction_id'] ?? ''));

        $return = [
            'amount' => $amount,
            'currencyCode' => $currencyCode,
        ];

        // Referans: settled/host key varsa onu tercih et, yoksa 24-char TDSC-prefixed orderID (PosNet.php:587-591).
        if (! empty($hostLogKey)) {
            $return['hostLogKey'] = (string) $hostLogKey;
        } else {
            $return['orderID'] = $this->mapPrefixedOrderId($params['order_id'] ?? '');
        }

        $correlationId = $this->formatOrderId($params['order_id'] ?? ('YKB' . time()));
        $xml = $this->buildPosnetXml(['return' => $return], true);
        $parsed = $this->send($xml, $correlationId);

        if ($parsed && ($parsed['approved'] ?? '0') === '1') {
            return [
                'status' => 'success',
                'message' => __('admin/modules.yapikredi_refund_success'),
                'refund_id' => $parsed['hostlogkey'] ?? '',
                'data' => $parsed,
            ];
        }

        return [
            'status' => 'error',
            'message' => $parsed['respText'] ?? __('admin/modules.yapikredi_refund_failed'),
            'error_code' => $parsed['respCode'] ?? '',
            'data' => $parsed,
        ];
    }

    /**
     * Odeme linki: Posnet hosted pay-link desteklemez.
     * Kart varsa 3DS baslat, yoksa NET 'desteklenmiyor' hatasi (sahte-success yok).
     *
     * @return array
     */
    public function generatePaymentLink(array $params)
    {
        if (isset($params['card_number'])) {
            return $this->start3DPayment($params);
        }

        return [
            'status' => 'error',
            'message' => __('admin/modules.yapikredi_payment_link_not_supported'),
        ];
    }

    /**
     * Test baglantisi - gercek 'agreement/status' op ile; basari yalnizca banka
     * envelope'unda BILINEN bir yanit (approved veya respCode) varsa (sahte-success yok).
     *
     * @return array
     */
    public function testConnection(array $params = [])
    {
        $configCheck = $this->validateConfig();
        if ($configCheck['status'] !== 'success') {
            return $configCheck;
        }

        // Referans status op = 'agreement' (PosNet.php:78,531-542). Var-olmayan siparis sorgusu:
        // banka BILINEN bir hata envelope'u (approved=0 + respCode) dondururse => baglanti CANLI.
        $probeOrderId = $this->mapPrefixedOrderId('TEST' . time());
        $body = [
            'agreement' => [
                'orderID' => $probeOrderId,
            ],
        ];

        $xml = $this->buildPosnetXml($body, false);
        $result = $this->sendRaw($xml, $probeOrderId);

        if ($result['error'] !== null || $result['raw_response'] === '') {
            return [
                'status' => 'error',
                'message' => __('admin/modules.yapikredi_connection_failed'),
            ];
        }

        $parsed = $this->parsePosnetResponse($result['raw_response']);

        // Sadece 'XML parse edildi' YETMEZ — gercek bir banka yanit alani araniyor.
        $reachedBank = is_array($parsed)
            && (($parsed['approved'] ?? null) !== null && ($parsed['approved'] ?? '') !== '')
            && (($parsed['respCode'] ?? '') !== '' || ($parsed['respText'] ?? '') !== '' || ($parsed['approved'] ?? '0') === '1');

        if ($reachedBank) {
            return [
                'status' => 'success',
                'message' => __('admin/modules.yapikredi_connection_success'),
            ];
        }

        return [
            'status' => 'error',
            'message' => __('admin/modules.yapikredi_connection_failed'),
        ];
    }

    /**
     * Konfigurasyon dogrulama
     *
     * @return array
     */
    public function validateConfig()
    {
        $required = ['merchant_id', 'terminal_id', 'posnet_id', 'enc_key'];
        $missing = [];

        foreach ($required as $field) {
            if (empty($this->config[$field])) {
                $missing[] = $field;
            }
        }

        if (! empty($missing)) {
            return [
                'status' => 'error',
                'message' => __('admin/modules.yapikredi_missing_config', ['fields' => implode(', ', $missing)]),
            ];
        }

        if (! ctype_digit((string) $this->getConfig('terminal_id'))) {
            return [
                'status' => 'error',
                'message' => __('admin/modules.yapikredi_invalid_terminal_id'),
            ];
        }

        if (! ctype_digit((string) $this->getConfig('merchant_id'))) {
            return [
                'status' => 'error',
                'message' => __('admin/modules.yapikredi_invalid_merchant_id'),
            ];
        }

        return [
            'status' => 'success',
            'message' => __('admin/modules.yapikredi_config_valid'),
        ];
    }

    /**
     * Modulun destekledigi ozellikleri dondurur
     */
    public function getFeatures(): array
    {
        return [
            'refund' => true,
            '3d_secure' => true,
            'save_card' => false,
            'recurring' => false,
            'fraud_check' => false,
            'installment' => true,
        ];
    }

    /**
     * Admin paneli icin konfigurasyon alanlari
     */
    public function getConfigFields(): array
    {
        return array_merge(parent::getConfigFields(), [
            'merchant_id' => [
                'type' => 'text',
                'label' => __('admin/modules.yapikredi_merchant_id'),
                'description' => __('admin/modules.yapikredi_merchant_id_desc'),
                'required' => true,
                'value' => '',
                'placeholder' => '6706598320',
            ],
            'terminal_id' => [
                'type' => 'text',
                'label' => __('admin/modules.yapikredi_terminal_id'),
                'description' => __('admin/modules.yapikredi_terminal_id_desc'),
                'required' => true,
                'value' => '',
                'placeholder' => '67005551',
            ],
            'posnet_id' => [
                'type' => 'text',
                'label' => __('admin/modules.yapikredi_posnet_id'),
                'description' => __('admin/modules.yapikredi_posnet_id_desc'),
                'required' => true,
                'value' => '',
                'placeholder' => '27426',
            ],
            'enc_key' => [
                'type' => 'password',
                'label' => __('admin/modules.yapikredi_enc_key'),
                'description' => __('admin/modules.yapikredi_enc_key_desc'),
                'required' => true,
                'value' => '',
                'placeholder' => '••••••••',
            ],
            'currency_code' => [
                'type' => 'select',
                'label' => __('admin/modules.yapikredi_currency_code'),
                'description' => __('admin/modules.yapikredi_currency_code_desc'),
                'required' => true,
                'value' => 'TL',
                'options' => [
                    'TL' => 'TRY - Turkish Lira',
                    'US' => 'USD - US Dollar',
                    'EU' => 'EUR - Euro',
                    'GB' => 'GBP - British Pound',
                ],
            ],
        ], $this->installmentConfigFields());
    }

    /**
     * API URL dondurur (test/live)
     */
    public function getApiUrl(): string
    {
        return $this->isTestMode()
            ? 'https://setmpos.ykb.com/PosnetWebService/XML'
            : 'https://posnet.yapikredi.com.tr/PosnetWebService/XML';
    }

    /**
     * 3D Gateway URL dondurur (test/live)
     */
    public function get3DGatewayUrl(): string
    {
        return $this->isTestMode()
            ? 'https://setmpos.ykb.com/3DSWebService/YKBPaymentService'
            : 'https://posnet.yapikredi.com.tr/3DSWebService/YKBPaymentService';
    }

    // =========================================================================
    // Private Helper Methods
    // =========================================================================

    /** Referans: sandbox'ta setmpos sabit store-key bekler → override (WiseCP Yapikredi.php:67-68,133-134). */
    private function storeKey(): string
    {
        return $this->isTestMode() ? '10,10,10,10,10,10,10,10' : (string) $this->getConfig('enc_key');
    }

    /** Referans hashString: base64(sha256_bin(str)) (PosNet.php:317-320). */
    private function hashString(string $str): string
    {
        return base64_encode(hash('sha256', $str, true));
    }

    /**
     * Referans create3DHash — İKİ geçiş (PosNet.php:327-338).
     * firstHash=base64(sha256(storeKey;terminalId)); mac=base64(sha256(orderId;amount;currency;clientId;firstHash)).
     */
    private function create3DHash(string $orderId, string $amount, string $currency, string $merchantId, string $storeKey): string
    {
        $terminalId = (string) $this->getConfig('terminal_id');
        $firstHash = $this->hashString($storeKey . ';' . $terminalId);

        return $this->hashString(implode(';', [$orderId, $amount, $currency, $merchantId, $firstHash]));
    }

    /** Referans verifyResponseMAC beklenen hash (PosNet.php:347-358) — mdStatus onekli. */
    private function verifyResponseMac(string $mdStatus, string $orderId, string $amount, string $currency, string $merchantId, string $storeKey): string
    {
        $terminalId = (string) $this->getConfig('terminal_id');
        $firstHash = $this->hashString($storeKey . ';' . $terminalId);

        return $this->hashString(implode(';', [$mdStatus, $orderId, $amount, $currency, $merchantId, $firstHash]));
    }

    /**
     * Finalize (oosTranData) response MAC beklenen hash (doküman §4, 2.0.1.4 satır 725):
     * HASH(hostLogkey ; xid ; amount ; currency ; merchantNo ; HASH(encKey;terminalID)).
     */
    private function finalizeResponseMac(string $hostLogKey, string $orderId, string $amount, string $currency, string $merchantId, string $storeKey): string
    {
        $terminalId = (string) $this->getConfig('terminal_id');
        $firstHash = $this->hashString($storeKey . ';' . $terminalId);

        return $this->hashString(implode(';', [$hostLogKey, $orderId, $amount, $currency, $merchantId, $firstHash]));
    }

    /**
     * Referans check3DHash (PosNet.php:626-653): MerchantPacket'i StoreKey ile 3DES-EDE3-CBC decrypt et,
     * ';' ile ayir, [clientId;terminalId;amount;(int)installment;orderId] katı `===` karşılaştır. FAIL-CLOSED.
     */
    private function check3DHash(string $merchantPacket, string $storeKey, string $merchantId, string $orderId, string $amount, string $installment): bool
    {
        if ($storeKey === '') {
            return false;
        }

        $decrypted = $this->posnetDecrypt($merchantPacket, $storeKey);
        if ($decrypted === false || $decrypted === '') {
            return false;
        }

        $parts = explode(';', $decrypted);
        if (count($parts) < 5) {
            return false;
        }

        $original = array_map('strval', [
            $merchantId,
            (string) $this->getConfig('terminal_id'),
            $amount,
            (int) $installment,
            $orderId,
        ]);

        $received = array_map('strval', [
            $parts[0],
            $parts[1],
            $parts[2],
            (int) $parts[3],
            $parts[4],
        ]);

        return $original === $received;
    }

    /**
     * Posnet 3DES-EDE3-CBC decrypt (referans PosNetCrypt::decrypt, PosNetCrypt.php:95-119).
     * Paket = IV(16 hex) + payload(hex) + CRC(8 hex). Anahtar = substr(strtoupper(md5(key)),0,24).
     */
    private function posnetDecrypt(string $data, string $key)
    {
        if (strlen($data) < 16 + 8) {
            return false;
        }

        $crc = substr($data, -8);
        $crcData = substr($data, 0, strlen($data) - 8);
        $ivHex = substr($data, 0, 16);
        $payloadHex = substr($data, 16, strlen($data) - 16 - 8);

        // CRC dogrula (PosNetCrypt.php:151-158).
        $calcCrc = strtoupper(sprintf('%08x', crc32($crcData)));
        if (strcmp($calcCrc, $crc) !== 0) {
            return false;
        }

        $iv = @pack('H*', $ivHex);
        $encrypted = @pack('H*', $payloadHex);
        if ($iv === false || $encrypted === false) {
            return false;
        }

        $detKey = substr(strtoupper(md5($key)), 0, 24);

        $result = openssl_decrypt($encrypted, 'des-ede3-cbc', $detKey, OPENSSL_RAW_DATA, $iv);

        return $result === false ? false : $result;
    }

    /** Referans amountFormat: round($amount,2)*100 (PosNet.php:385-388). */
    private function amountMinor($amount, $preMinor = null): string
    {
        if ($preMinor !== null && $preMinor !== '') {
            return (string) (int) $preMinor;
        }

        return (string) (int) round(((float) $amount) * 100);
    }

    /**
     * ISO para birimini Posnet formuna map'ler (referans convert_curr — WiseCP Yapikredi.php:41-53).
     * Caller ISO enjekte ediyorsa (TRY/USD/EUR/GBP) POSnet formuna (TL/US/EU/GB) çevir; zaten POSnet
     * formundaysa aynen bırak; hiçbiri değilse config'teki currency_code'a (varsayılan TL) düş.
     * Request XML currencyCode'u ve MAC/create3DHash AYNI değeri kullanmalı (banka ile eşleşme şartı).
     */
    private function posnetCurrency($currency): string
    {
        $map = ['TRY' => 'TL', 'USD' => 'US', 'EUR' => 'EU', 'GBP' => 'GB'];
        $posnetForms = ['TL', 'US', 'EU', 'GB'];

        $currency = strtoupper(trim((string) $currency));

        if ($currency !== '' && isset($map[$currency])) {
            return $map[$currency];
        }

        if (in_array($currency, $posnetForms, true)) {
            return $currency;
        }

        return (string) $this->getConfig('currency_code', 'TL');
    }

    /** Referans formatInstallment: 00 veya 02/06... (PosNet.php:421-428). */
    private function formatInstallment($installment): string
    {
        $installment = (int) $installment;

        return $installment > 1 ? str_pad((string) $installment, 2, '0', STR_PAD_LEFT) : '00';
    }

    /** exp: YYMM (referans CreditCardPosNet::getExpirationDate). */
    private function formatExpDate(array $params): string
    {
        return substr((string) $params['exp_year'], -2) . str_pad((string) $params['exp_month'], 2, '0', STR_PAD_LEFT);
    }

    /** Referans formatOrderId: sol sıfır-pad 20 (PosNet.php:368-375). */
    private function formatOrderId($orderId): string
    {
        return str_pad((string) $orderId, self::ORDER_ID_LENGTH, '0', STR_PAD_LEFT);
    }

    /** Referans mapOrderIdToPrefixedOrderId: 3D için TDSC + pad 24 (PosNet.php:401-411). */
    private function mapPrefixedOrderId($orderId): string
    {
        $prefix = self::ORDER_ID_3D_PREFIX;

        return $prefix . str_pad((string) $orderId, self::ORDER_ID_TOTAL_LENGTH - strlen($prefix), '0', STR_PAD_LEFT);
    }

    /**
     * Posnet XML olusturur. Referans createXML(..., 'ISO-8859-9') (PosNet.php:134).
     * mid+tid otomatik; $withTranDate ise tranDateRequired=1 (PosNet.php:438).
     */
    private function buildPosnetXml(array $body, bool $withTranDate): string
    {
        $merchantId = (string) $this->getConfig('merchant_id');
        $terminalId = (string) $this->getConfig('terminal_id');

        $inner = '<mid>' . $this->xmlEscape($merchantId) . '</mid>'
            . '<tid>' . $this->xmlEscape($terminalId) . '</tid>';

        if ($withTranDate) {
            $inner .= '<tranDateRequired>1</tranDateRequired>';
        }

        foreach ($body as $op => $fields) {
            $inner .= '<' . $op . '>';
            foreach ($fields as $key => $value) {
                $inner .= '<' . $key . '>' . $this->xmlEscape((string) $value) . '</' . $key . '>';
            }
            $inner .= '</' . $op . '>';
        }

        $xml = '<?xml version="1.0" encoding="ISO-8859-9"?>'
            . '<posnetRequest>' . $inner . '</posnetRequest>';

        // Govde ISO-8859-9 olmalı (Turkce karakter/hash uyumu). UTF-8'den donustur.
        $encoded = @mb_convert_encoding($xml, 'ISO-8859-9', 'UTF-8');

        return $encoded !== false ? $encoded : $xml;
    }

    /**
     * XML gonderir + parse eder. Transport = postForm(url, ['xmldata'=>$xml]) — PRC-888 hardening.
     * $correlationId (işleme özel tekil değer, satış için XID) X-CORRELATION-ID header'ına konur.
     */
    private function send(string $xml, string $correlationId = ''): ?array
    {
        $result = $this->sendRaw($xml, $correlationId);
        if ($result['error'] !== null || $result['raw_response'] === '') {
            return null;
        }

        return $this->parsePosnetResponse($result['raw_response']);
    }

    /**
     * Ham POST (postForm ile). Referans body: xmldata=<xml> form-urlencoded (PosNet.php:278).
     * POSNET 2.0.1.5 sözleşmesi (doküman s.3 + Canlı Ortama Geçiş): her request header'ına
     * X-MERCHANT-ID / X-TERMINAL-ID / X-POSNET-ID / X-CORRELATION-ID eklenmeli (canlı ZORUNLU).
     *
     * @return array{raw_response:string,error:?string}
     */
    private function sendRaw(string $xml, string $correlationId = ''): array
    {
        $result = $this->postForm($this->apiUrl, ['xmldata' => $xml], $this->serviceHeaders($correlationId));

        if (($result['error'] ?? null) !== null) {
            \Illuminate\Support\Facades\Log::error('Yapikredi Posnet API transport error', [
                'url' => $this->apiUrl,
                'error' => $result['error'],
            ]);
        }

        return [
            'raw_response' => (string) ($result['raw_response'] ?? ''),
            'error' => $result['error'] ?? null,
        ];
    }

    /**
     * POSNET zorunlu servis header'ları (doküman s.3 satır 128-130 + Canlı Ortama Geçiş 1062-1063).
     * CorrelationId işleme özel tekil değerdir (satış işlemleri için XID/orderID set edilebilir).
     */
    private function serviceHeaders(string $correlationId): array
    {
        $headers = [
            'X-MERCHANT-ID: ' . (string) $this->getConfig('merchant_id'),
            'X-TERMINAL-ID: ' . (string) $this->getConfig('terminal_id'),
            'X-POSNET-ID: ' . (string) $this->getConfig('posnet_id'),
        ];

        if ($correlationId !== '') {
            $headers[] = 'X-CORRELATION-ID: ' . $correlationId;
        }

        return $headers;
    }

    /** Banka yanitini ortak zarfa map'ler. */
    private function mapPaymentResponse(?array $parsed, string $orderId): array
    {
        if ($parsed && ($parsed['approved'] ?? '0') === '1') {
            return [
                'status' => 'success',
                'message' => __('admin/modules.yapikredi_payment_success'),
                'transaction_id' => $parsed['hostlogkey'] ?? $orderId,
                'auth_code' => $parsed['authCode'] ?? '',
                'order_id' => $orderId,
                'data' => $parsed,
            ];
        }

        return [
            'status' => 'error',
            'message' => ($parsed['respText'] ?? null) ?: __('admin/modules.yapikredi_payment_failed'),
            'error_code' => $parsed['respCode'] ?? '',
            'data' => $parsed,
        ];
    }

    /**
     * Posnet XML yanitini parse eder. oosRequestDataResponse.{data1,data2,sign} +
     * oosResolveMerchantDataResponse.{mdStatus,mac,xid,...}.
     */
    private function parsePosnetResponse(string $rawResponse): ?array
    {
        if (empty($rawResponse)) {
            return null;
        }

        libxml_use_internal_errors(true);
        $xml = simplexml_load_string($rawResponse);
        libxml_clear_errors();

        if ($xml === false) {
            return null;
        }

        $result = [
            'approved' => (string) ($xml->approved ?? '0'),
            'respCode' => (string) ($xml->respCode ?? ''),
            'respText' => (string) ($xml->respText ?? ''),
        ];

        foreach (['hostlogkey', 'authCode', 'instInfo', 'pointInfo'] as $k) {
            if (isset($xml->$k)) {
                $result[$k] = (string) $xml->$k;
            }
        }

        // Top-level <mac> — finalize (oosTranData) cevabında döner (doküman §4). Resolve
        // cevabındaki mac ise oosResolveMerchantDataResponse içindedir (aşağıda override eder).
        if (isset($xml->mac)) {
            $result['mac'] = (string) $xml->mac;
        }

        // OOS 3D initiation yaniti (referans op oosRequestDataResponse, PosNet.php:244-248).
        if (isset($xml->oosRequestDataResponse)) {
            $oos = $xml->oosRequestDataResponse;
            $result['data1'] = (string) ($oos->data1 ?? '');
            $result['data2'] = (string) ($oos->data2 ?? '');
            $result['sign'] = (string) ($oos->sign ?? '');
        }

        // OOS Resolve yaniti (mac dahil — verifyResponseMAC icin).
        if (isset($xml->oosResolveMerchantDataResponse)) {
            $r = $xml->oosResolveMerchantDataResponse;
            $result['xid'] = (string) ($r->xid ?? '');
            $result['amount'] = (string) ($r->amount ?? '');
            $result['currency'] = (string) ($r->currency ?? '');
            $result['installment'] = (string) ($r->installment ?? '');
            $result['mdStatus'] = (string) ($r->mdStatus ?? '');
            $result['mdErrorMessage'] = (string) ($r->mdErrorMessage ?? '');
            $result['mac'] = (string) ($r->mac ?? '');
            $result['txStatus'] = (string) ($r->txStatus ?? '');
            $result['eci'] = (string) ($r->eci ?? '');
            $result['cavv'] = (string) ($r->cavv ?? '');
        }

        return $result;
    }

    /** XML icin ozel karakterleri escape eder. */
    private function xmlEscape(string $value): string
    {
        return htmlspecialchars($value, ENT_XML1 | ENT_QUOTES, 'UTF-8');
    }

    /** Luhn algoritmasi ile kart numarasi dogrulama */
    private function isValidLuhn(string $number): bool
    {
        $number = preg_replace('/\D/', '', $number);

        if (strlen($number) < 13 || strlen($number) > 19) {
            return false;
        }

        $sum = 0;
        $alternate = false;

        for ($i = strlen($number) - 1; $i >= 0; $i--) {
            $digit = (int) $number[$i];

            if ($alternate) {
                $digit *= 2;
                if ($digit > 9) {
                    $digit -= 9;
                }
            }

            $sum += $digit;
            $alternate = ! $alternate;
        }

        return $sum % 10 === 0;
    }

    public function getSupportedFeatures(): array
    {
        return $this->getFeatures();
    }
}
