<?php

namespace App\Modules\Payment\PaymentExpress;

use App\Core\Payment\AbstractPayment;

class PaymentExpress extends AbstractPayment
{
    // WHMCS gateways/paymentexpress.php:23 = /pxpay/pxaccess.aspx (legacy host, path was reversed before)
    protected $pxPayUrl = 'https://sec.paymentexpress.com/pxpay/pxaccess.aspx';

    protected $pxPostUrl = 'https://sec.paymentexpress.com/pxpost.aspx';

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

    /**
     * Validate payment parameters and return ready status
     */
    public function attemptPayment(array $params)
    {
        $requiredParams = ['amount', 'currency', 'order_id'];

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

        return [
            'status' => 'ready',
            'message' => __('admin/modules.paymentexpress_payment_ready'),
            'data' => [
                'amount' => $params['amount'],
                'currency' => $params['currency'],
                'order_id' => $params['order_id'],
            ],
        ];
    }

    /**
     * Build PxPay XML request and redirect user to hosted payment page
     */
    public function makePayment(array $params)
    {
        $currency = $params['currency'] ?? $this->getConfig('currency', 'NZD');
        $amount = number_format((float) ($params['amount'] ?? 0), 2, '.', '');
        $orderId = $params['order_id'] ?? '';
        $successUrl = $params['success_url'] ?? $params['redirect_url'] ?? '';
        $failUrl = $params['fail_url'] ?? $params['cancel_url'] ?? $successUrl;
        $description = $params['description'] ?? __('admin/modules.paymentexpress_payment_description', ['order_id' => $orderId]);

        // WHMCS paymentexpress.php:24 — TxnData1=reconciliation ref (order_id), TxnId=idempotency, EmailAddress optional
        $fields = [
            'AmountInput' => $amount,
            'CurrencyInput' => strtoupper($currency),
            'MerchantReference' => $description,
            'TxnType' => 'Purchase',
            'UrlSuccess' => $successUrl,
            'UrlFail' => $failUrl,
            'EnableAddBillCard' => '1',
            'TxnData1' => $orderId,
            'TxnId' => substr(preg_replace('/[^A-Za-z0-9]/', '', time() . $orderId), 0, 16),
        ];
        if (! empty($params['email'])) {
            $fields['EmailAddress'] = $params['email'];
        }

        $xml = $this->buildPxPayXml($fields);

        $response = $this->callPxPay($xml);

        if ($response === false) {
            return [
                'status' => 'error',
                'message' => __('admin/modules.paymentexpress_request_failed'),
            ];
        }

        $parsed = $this->parsePxPayResponse($response);

        if ($parsed === false) {
            return [
                'status' => 'error',
                'message' => __('admin/modules.paymentexpress_invalid_response'),
            ];
        }

        if (isset($parsed['valid']) && $parsed['valid'] === '1' && ! empty($parsed['URI'])) {
            return [
                'status' => 'redirect',
                'message' => __('admin/modules.paymentexpress_redirect'),
                'redirect_url' => $parsed['URI'],
                'data' => [
                    'redirect_url' => $parsed['URI'],
                    'amount' => $amount,
                    'currency' => $currency,
                    'order_id' => $orderId,
                ],
            ];
        }

        return [
            'status' => 'error',
            'message' => __('admin/modules.paymentexpress_request_failed'),
            'data' => $parsed,
        ];
    }

    /**
     * Handle callback from PxPay - send ProcessResponse XML to verify transaction
     */
    public function handlePaymentCallback(array $params)
    {
        $responseToken = $params['result'] ?? $params['Response'] ?? null;

        if (! $responseToken) {
            return [
                'status' => 'failed',
                'message' => __('admin/modules.paymentexpress_missing_response_token'),
            ];
        }

        $xml = '<ProcessResponse>'
            . '<PxPayUserId>' . htmlspecialchars($this->getConfig('pxpay_userid', '')) . '</PxPayUserId>'
            . '<PxPayKey>' . htmlspecialchars($this->getConfig('pxpay_key', '')) . '</PxPayKey>'
            . '<Response>' . htmlspecialchars($responseToken) . '</Response>'
            . '</ProcessResponse>';

        $response = $this->callPxPay($xml);

        if ($response === false) {
            return [
                'status' => 'failed',
                'message' => __('admin/modules.paymentexpress_callback_failed'),
            ];
        }

        $parsed = $this->parsePxPayResponse($response);

        if ($parsed === false) {
            return [
                'status' => 'failed',
                'message' => __('admin/modules.paymentexpress_invalid_response'),
            ];
        }

        $success = ($parsed['Success'] ?? '0') === '1';

        if ($success) {
            // Fail-closed reconciliation: PxPay verify (ProcessResponse) confirms the
            // transaction is genuine, but the settled amount/currency must still match
            // the originating order before we mark it paid. Guards against a low-amount
            // replay / mismatched-token settlement. Skipped only when the caller did not
            // supply an expected value (nothing to compare against).
            $reconcileError = $this->reconcileSettlement($parsed, $params);
            if ($reconcileError !== null) {
                return [
                    'status' => 'failed',
                    'message' => __('admin/modules.paymentexpress_amount_mismatch'),
                    'data' => [
                        'reason' => $reconcileError,
                        'amount' => $parsed['AmountSettlement'] ?? '',
                        'currency' => $parsed['CurrencySettlement'] ?? '',
                        'merchant_reference' => $parsed['MerchantReference'] ?? '',
                        'order_id' => $parsed['TxnData1'] ?? '',
                    ],
                ];
            }

            $result = [
                'status' => 'success',
                'message' => __('admin/modules.paymentexpress_payment_success'),
                'transaction_id' => $parsed['DpsTxnRef'] ?? '',
                'data' => [
                    'dps_txn_ref' => $parsed['DpsTxnRef'] ?? '',
                    'response_text' => $parsed['ResponseText'] ?? '',
                    'auth_code' => $parsed['AuthCode'] ?? '',
                    'card_number' => $parsed['CardNumber'] ?? '',
                    'card_name' => $parsed['CardHolderName'] ?? '',
                    'amount' => $parsed['AmountSettlement'] ?? '',
                    'currency' => $parsed['CurrencySettlement'] ?? '',
                    'merchant_reference' => $parsed['MerchantReference'] ?? '',
                    // WHMCS callback:30 reads TxnData1 back as invoiceid for reconciliation
                    'order_id' => $parsed['TxnData1'] ?? '',
                ],
            ];

            // Include DpsBillingId for token storage if available
            if (! empty($parsed['DpsBillingId'])) {
                $result['data']['dps_billing_id'] = $parsed['DpsBillingId'];
                $result['data']['card_token'] = $parsed['DpsBillingId'];
            }

            return $result;
        }

        return [
            'status' => 'failed',
            'message' => __('admin/modules.paymentexpress_payment_declined'),
            'data' => [
                'response_text' => $parsed['ResponseText'] ?? '',
                'dps_txn_ref' => $parsed['DpsTxnRef'] ?? '',
                'merchant_reference' => $parsed['MerchantReference'] ?? '',
            ],
        ];
    }

    /**
     * Refund a transaction via PxPost
     */
    public function refundPayment(array $params)
    {
        $dpsTxnRef = $params['transaction_id'] ?? $params['dps_txn_ref'] ?? '';
        $amount = number_format((float) ($params['amount'] ?? 0), 2, '.', '');

        if (empty($dpsTxnRef)) {
            return [
                'status' => 'error',
                'message' => __('admin/modules.paymentexpress_missing_txn_ref'),
            ];
        }

        // Windcave PxPost uses a SEPARATE account (PostUsername/PostPassword), NOT the PxPay UserId/Key
        $xml = '<Txn>'
            . '<PostUsername>' . htmlspecialchars($this->getConfig('pxpost_username', '')) . '</PostUsername>'
            . '<PostPassword>' . htmlspecialchars($this->getConfig('pxpost_password', '')) . '</PostPassword>'
            . '<TxnType>Refund</TxnType>'
            . '<DpsTxnRef>' . htmlspecialchars($dpsTxnRef) . '</DpsTxnRef>'
            . '<Amount>' . $amount . '</Amount>'
            . '</Txn>';

        $response = $this->callPxPost($xml);

        if ($response === false) {
            return [
                'status' => 'error',
                'message' => __('admin/modules.paymentexpress_refund_failed'),
            ];
        }

        $parsed = $this->parsePxPostResponse($response);

        if ($parsed === false) {
            return [
                'status' => 'error',
                'message' => __('admin/modules.paymentexpress_invalid_response'),
            ];
        }

        $success = ($parsed['Success'] ?? '0') === '1';

        if ($success) {
            return [
                'status' => 'success',
                'message' => __('admin/modules.paymentexpress_refund_success'),
                'data' => [
                    'refund_id' => $parsed['DpsTxnRef'] ?? '',
                    'response_text' => $parsed['ResponseText'] ?? '',
                ],
            ];
        }

        return [
            'status' => 'error',
            'message' => __('admin/modules.paymentexpress_refund_failed'),
            'data' => [
                'response_text' => $parsed['ResponseText'] ?? '',
                'help_text' => $parsed['HelpText'] ?? '',
            ],
        ];
    }

    /**
     * Generate a payment link via PxPay request
     */
    public function generatePaymentLink(array $params)
    {
        $currency = $params['currency'] ?? $this->getConfig('currency', 'NZD');
        $amount = number_format((float) ($params['amount'] ?? 0), 2, '.', '');
        $orderId = $params['order_id'] ?? '';
        $successUrl = $params['success_url'] ?? $params['redirect_url'] ?? '';
        $failUrl = $params['fail_url'] ?? $params['cancel_url'] ?? $successUrl;

        $xml = $this->buildPxPayXml([
            'AmountInput' => $amount,
            'CurrencyInput' => strtoupper($currency),
            'MerchantReference' => $orderId,
            'TxnType' => 'Purchase',
            'UrlSuccess' => $successUrl,
            'UrlFail' => $failUrl,
            'EnableAddBillCard' => '1',
        ]);

        $response = $this->callPxPay($xml);

        if ($response === false) {
            return [
                'status' => 'error',
                'message' => __('admin/modules.paymentexpress_payment_link_failed'),
            ];
        }

        $parsed = $this->parsePxPayResponse($response);

        if ($parsed !== false && isset($parsed['valid']) && $parsed['valid'] === '1' && ! empty($parsed['URI'])) {
            return [
                'status' => 'success',
                'payment_url' => $parsed['URI'],
                'data' => [
                    'redirect_url' => $parsed['URI'],
                    'amount' => $amount,
                    'currency' => $currency,
                    'order_id' => $orderId,
                ],
            ];
        }

        return [
            'status' => 'error',
            'message' => __('admin/modules.paymentexpress_payment_link_failed'),
            'data' => $parsed,
        ];
    }

    /**
     * Save card using DpsBillingId from a previous payment response
     */
    public function saveCard(array $params)
    {
        $dpsBillingId = $params['dps_billing_id'] ?? $params['card_token'] ?? null;

        if (empty($dpsBillingId)) {
            return [
                'status' => 'error',
                'message' => __('admin/modules.paymentexpress_no_billing_id'),
            ];
        }

        // PCI: never echo raw PAN. PxPay already returns a masked CardNumber; mask any caller-supplied value.
        $data = [
            'card_token' => $dpsBillingId,
            'dps_billing_id' => $dpsBillingId,
            'card_name' => $params['card_name'] ?? '',
        ];
        if (! empty($params['card_number'])) {
            $data['card_number'] = $this->maskCardNumber($params['card_number']);
        }

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

    /**
     * Process payment using saved DpsBillingId via PxPost
     */
    public function payWithSavedCard(array $params)
    {
        $dpsBillingId = $params['dps_billing_id'] ?? $params['card_token'] ?? '';
        $amount = number_format((float) ($params['amount'] ?? 0), 2, '.', '');
        $currency = $params['currency'] ?? $this->getConfig('currency', 'NZD');
        $orderId = $params['order_id'] ?? '';

        if (empty($dpsBillingId)) {
            return [
                'status' => 'error',
                'message' => __('admin/modules.paymentexpress_no_billing_id'),
            ];
        }

        // Windcave PxPost rebill uses the SEPARATE PxPost account credentials, NOT PxPay UserId/Key
        $xml = '<Txn>'
            . '<PostUsername>' . htmlspecialchars($this->getConfig('pxpost_username', '')) . '</PostUsername>'
            . '<PostPassword>' . htmlspecialchars($this->getConfig('pxpost_password', '')) . '</PostPassword>'
            . '<TxnType>Purchase</TxnType>'
            . '<Amount>' . $amount . '</Amount>'
            . '<InputCurrency>' . htmlspecialchars(strtoupper($currency)) . '</InputCurrency>'
            . '<DpsBillingId>' . htmlspecialchars($dpsBillingId) . '</DpsBillingId>'
            . '<MerchantReference>' . htmlspecialchars($orderId) . '</MerchantReference>'
            . '</Txn>';

        $response = $this->callPxPost($xml);

        if ($response === false) {
            return [
                'status' => 'error',
                'message' => __('admin/modules.paymentexpress_payment_failed'),
            ];
        }

        $parsed = $this->parsePxPostResponse($response);

        if ($parsed === false) {
            return [
                'status' => 'error',
                'message' => __('admin/modules.paymentexpress_invalid_response'),
            ];
        }

        $success = ($parsed['Success'] ?? '0') === '1';

        if ($success) {
            return [
                'status' => 'success',
                'message' => __('admin/modules.paymentexpress_payment_success'),
                'data' => [
                    'transaction_id' => $parsed['DpsTxnRef'] ?? '',
                    'response_text' => $parsed['ResponseText'] ?? '',
                    'auth_code' => $parsed['AuthCode'] ?? '',
                ],
            ];
        }

        return [
            'status' => 'failed',
            'message' => __('admin/modules.paymentexpress_payment_declined'),
            'data' => [
                'response_text' => $parsed['ResponseText'] ?? '',
                'help_text' => $parsed['HelpText'] ?? '',
            ],
        ];
    }

    /**
     * Delete card - not directly supported by Windcave
     * Token can be discarded locally
     */
    public function deleteCard(array $params)
    {
        return [
            'status' => 'success',
            'message' => __('admin/modules.paymentexpress_card_deleted'),
        ];
    }

    /**
     * Test connection by sending a minimal GenerateRequest to PxPay
     */
    public function testConnection(array $params = [])
    {
        $xml = $this->buildPxPayXml([
            'AmountInput' => '1.00',
            'CurrencyInput' => $this->getConfig('currency', 'NZD'),
            'MerchantReference' => 'connection_test',
            'TxnType' => 'Purchase',
            'UrlSuccess' => 'https://localhost/success',
            'UrlFail' => 'https://localhost/fail',
        ]);

        $response = $this->callPxPay($xml);

        if ($response === false) {
            return [
                'status' => 'error',
                'message' => __('admin/modules.paymentexpress_connection_failed'),
            ];
        }

        $parsed = $this->parsePxPayResponse($response);

        if ($parsed !== false && isset($parsed['valid']) && $parsed['valid'] === '1') {
            return [
                'status' => 'success',
                'message' => __('admin/modules.paymentexpress_connection_success'),
            ];
        }

        return [
            'status' => 'error',
            'message' => __('admin/modules.paymentexpress_connection_failed'),
            'data' => $parsed,
        ];
    }

    /**
     * Validate required configuration fields
     */
    public function validateConfig()
    {
        $required = ['pxpay_userid', 'pxpay_key'];
        $missing = [];

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

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

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

    /**
     * Supported features
     */
    public function getFeatures(): array
    {
        return [
            'refund' => true,
            '3d_secure' => false,
            'save_card' => true,
            'recurring' => true,
            'fraud_check' => false,
        ];
    }

    /**
     * Return supported features (alias)
     */
    public function getSupportedFeatures(): array
    {
        return $this->getFeatures();
    }

    /**
     * Get configuration fields for admin panel
     */
    public function getConfigFields(): array
    {
        return array_merge(parent::getConfigFields(), [
            'pxpay_userid' => [
                'type' => 'text',
                'label' => __('admin/modules.paymentexpress_pxpay_userid'),
                'description' => __('admin/modules.paymentexpress_pxpay_userid_description'),
                'required' => true,
                'value' => '',
                'placeholder' => __('admin/modules.paymentexpress_pxpay_userid_placeholder'),
            ],
            'pxpay_key' => [
                'type' => 'password',
                'label' => __('admin/modules.paymentexpress_pxpay_key'),
                'description' => __('admin/modules.paymentexpress_pxpay_key_description'),
                'required' => true,
                'value' => '',
                'placeholder' => __('admin/modules.paymentexpress_pxpay_key_placeholder'),
            ],
            'pxpost_username' => [
                'type' => 'text',
                'label' => __('admin/modules.paymentexpress_pxpost_username'),
                'description' => __('admin/modules.paymentexpress_pxpost_username_description'),
                'required' => false,
                'value' => '',
                'placeholder' => __('admin/modules.paymentexpress_pxpost_username_placeholder'),
            ],
            'pxpost_password' => [
                'type' => 'password',
                'label' => __('admin/modules.paymentexpress_pxpost_password'),
                'description' => __('admin/modules.paymentexpress_pxpost_password_description'),
                'required' => false,
                'value' => '',
                'placeholder' => __('admin/modules.paymentexpress_pxpost_password_placeholder'),
            ],
            'currency' => [
                'type' => 'select',
                'label' => __('admin/modules.paymentexpress_currency'),
                'description' => __('admin/modules.paymentexpress_currency_description'),
                'required' => true,
                'value' => 'NZD',
                'options' => [
                    'NZD' => 'NZD - New Zealand Dollar',
                    'AUD' => 'AUD - Australian Dollar',
                    'USD' => 'USD - US Dollar',
                    'GBP' => 'GBP - British Pound',
                    'EUR' => 'EUR - Euro',
                ],
            ],
        ]);
    }

    // -------------------------------------------------------------------------
    // Private Helpers
    // -------------------------------------------------------------------------

    /**
     * Verify the settled amount / currency / order reference from the PxPay
     * ProcessResponse against the expected order values supplied by the caller.
     *
     * Fail-closed: any *supplied* expected value that does not match rejects the
     * callback. When the caller supplies no expectation for a field, that field
     * is not enforced (nothing to reconcile against). Returns null when every
     * supplied expectation matches, otherwise a short machine-readable reason.
     */
    private function reconcileSettlement(array $parsed, array $params): ?string
    {
        // Expected amount (order total). Accept common param aliases.
        $expectedAmount = $params['expected_amount'] ?? $params['amount'] ?? null;
        if ($expectedAmount !== null && $expectedAmount !== '') {
            $settled = $parsed['AmountSettlement'] ?? '';
            if ($settled === '') {
                return 'settled_amount_missing';
            }
            // Compare as fixed 2-decimal strings to avoid float drift.
            $exp = number_format((float) $expectedAmount, 2, '.', '');
            $got = number_format((float) $settled, 2, '.', '');
            if (! $this->secureCompare($exp, $got)) {
                return 'amount_mismatch';
            }
        }

        // Expected currency (3-letter ISO).
        $expectedCurrency = $params['expected_currency'] ?? $params['currency'] ?? null;
        if ($expectedCurrency !== null && $expectedCurrency !== '') {
            $settledCurrency = $parsed['CurrencySettlement'] ?? '';
            if ($settledCurrency === ''
                || ! $this->secureCompare(strtoupper((string) $expectedCurrency), strtoupper($settledCurrency))) {
                return 'currency_mismatch';
            }
        }

        // Expected order reference. PxPay echoes the order back via TxnData1
        // (set as our reconciliation ref at GenerateRequest time). Fall back to
        // MerchantReference for callers that only carry the reference there.
        $expectedOrder = $params['expected_order_id'] ?? $params['order_id'] ?? null;
        if ($expectedOrder !== null && $expectedOrder !== '') {
            $echoedOrder = $parsed['TxnData1'] ?? '';
            $echoedRef = $parsed['MerchantReference'] ?? '';
            $matchesTxnData = $echoedOrder !== '' && $this->secureCompare((string) $expectedOrder, $echoedOrder);
            $matchesRef = $echoedRef !== '' && $this->secureCompare((string) $expectedOrder, $echoedRef);
            if (! $matchesTxnData && ! $matchesRef) {
                return 'order_mismatch';
            }
        }

        return null;
    }

    /**
     * Build PxPay GenerateRequest XML
     */
    private function buildPxPayXml(array $fields): string
    {
        $xml = '<GenerateRequest>'
            . '<PxPayUserId>' . htmlspecialchars($this->getConfig('pxpay_userid', '')) . '</PxPayUserId>'
            . '<PxPayKey>' . htmlspecialchars($this->getConfig('pxpay_key', '')) . '</PxPayKey>';

        foreach ($fields as $tag => $value) {
            $xml .= '<' . $tag . '>' . htmlspecialchars($value) . '</' . $tag . '>';
        }

        $xml .= '</GenerateRequest>';

        return $xml;
    }

    /**
     * Parse PxPay XML response into associative array
     * Handles both GenerateRequest response (Request tag with valid attribute) and ProcessResponse
     */
    private function parsePxPayResponse(string $xmlString)
    {
        libxml_use_internal_errors(true);
        $xml = simplexml_load_string($xmlString);

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

        $result = [];

        // Get attributes from root element (e.g. valid="1" on <Request>)
        foreach ($xml->attributes() as $attrName => $attrValue) {
            $result[$attrName] = (string) $attrValue;
        }

        // Get child elements
        foreach ($xml->children() as $child) {
            $result[$child->getName()] = (string) $child;
        }

        return $result;
    }

    /**
     * Parse PxPost XML response into associative array
     */
    private function parsePxPostResponse(string $xmlString)
    {
        libxml_use_internal_errors(true);
        $xml = simplexml_load_string($xmlString);

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

        $result = [];

        // Get attributes from root element
        foreach ($xml->attributes() as $attrName => $attrValue) {
            $result[$attrName] = (string) $attrValue;
        }

        // Get child elements
        foreach ($xml->children() as $child) {
            $result[$child->getName()] = (string) $child;
        }

        return $result;
    }

    /**
     * Send XML request to PxPay endpoint
     *
     * @return string|false Raw XML response or false on failure
     */
    private function callPxPay(string $xml)
    {
        $ch = curl_init();
        curl_setopt($ch, CURLOPT_URL, $this->pxPayUrl);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($ch, CURLOPT_POST, true);
        curl_setopt($ch, CURLOPT_POSTFIELDS, $xml);
        curl_setopt($ch, CURLOPT_TIMEOUT, 30);
        curl_setopt($ch, CURLOPT_HTTPHEADER, [
            'Content-Type: application/xml',
            'User-Agent: ProCP Payment Gateway Module',
        ]);

        $response = curl_exec($ch);
        $curlError = curl_error($ch);
        curl_close($ch);

        if ($curlError || $response === false) {
            return false;
        }

        return $response;
    }

    /**
     * Send XML request to PxPost endpoint
     *
     * @return string|false Raw XML response or false on failure
     */
    private function callPxPost(string $xml)
    {
        $ch = curl_init();
        curl_setopt($ch, CURLOPT_URL, $this->pxPostUrl);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($ch, CURLOPT_POST, true);
        curl_setopt($ch, CURLOPT_POSTFIELDS, $xml);
        curl_setopt($ch, CURLOPT_TIMEOUT, 30);
        curl_setopt($ch, CURLOPT_HTTPHEADER, [
            'Content-Type: application/xml',
            'User-Agent: ProCP Payment Gateway Module',
        ]);

        $response = curl_exec($ch);
        $curlError = curl_error($ch);
        curl_close($ch);

        if ($curlError || $response === false) {
            return false;
        }

        return $response;
    }
}
