<?php

namespace App\Modules\Payment\PinPayments;

use App\Core\Payment\AbstractPayment;

class PinPayments extends AbstractPayment
{
    protected $apiUrl;

    public function __construct(array $config = [])
    {
        parent::__construct($config);
        $this->apiUrl = $this->getApiUrl();
    }

    /**
     * Validate payment parameters before processing
     */
    public function attemptPayment(array $params)
    {
        $requiredParams = ['amount', 'currency'];

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

        if (empty($params['card_token']) && empty($params['customer_token'])) {
            return [
                'status' => 'error',
                'message' => __('admin/modules.pinpayments_token_required'),
                'data' => [],
            ];
        }

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

    /**
     * Process a charge via Pin Payments Charges API
     * Amount must be in cents (e.g. 100 = 1.00 AUD)
     * When a 3DS callback_url is available, sends `three_d_secure` and
     * returns a redirect action (ref: pinpayments.com Charges API 3DS flow).
     */
    public function makePayment(array $params)
    {
        $currency = strtoupper($params['currency'] ?? $this->getConfig('currency', 'AUD'));

        $chargeData = [
            'email' => $params['email'] ?? '',
            'description' => $params['description'] ?? __('admin/modules.pinpayments_charge_description', ['order_id' => $params['order_id'] ?? '']),
            'amount' => (int) round($params['amount'] * 100),
            'currency' => $currency,
            'ip_address' => $params['ip_address'] ?? request()->ip(),
        ];

        if (! empty($params['customer_token'])) {
            $chargeData['customer_token'] = $params['customer_token'];
        } elseif (! empty($params['card_token'])) {
            $chargeData['card_token'] = $params['card_token'];
        }

        if (! empty($params['metadata'])) {
            $chargeData['metadata'] = $params['metadata'];
        }

        // 2D vs 3D split — the admin `security_type` config gates 3DS, not just
        // callback presence. `2d` never attaches three_d_secure (direct sale);
        // `3d`/`auto` (default) attach it when a callback_url is available.
        // Per-charge override via $params['security_type'] (e.g. 'no_3d'/'2d').
        $securityType = strtolower((string) ($params['security_type'] ?? $this->getConfig('security_type', '3d')));
        $threeDEnabled = ! in_array($securityType, ['2d', 'no_3d', 'off'], true);

        // Ref: Charges API `three_d_secure` object — {enabled, fallback_ok, callback_url}
        $callbackUrl = $params['3ds_callback_url'] ?? $params['callback_url'] ?? null;
        if ($threeDEnabled && ! empty($callbackUrl)) {
            $chargeData['three_d_secure'] = [
                'enabled' => true,
                'fallback_ok' => true,
                'callback_url' => $callbackUrl,
            ];
        }

        $response = $this->callPinApi('/charges', 'POST', $chargeData);

        // 3DS flow: Pin returns 202 Accepted with a redirect_url the customer
        // must visit; charge is finalized later via complete3DPayment.
        if ($response['http_code'] === 202 && isset($response['response']['response'])) {
            $charge = $response['response']['response'];
            $tds = $charge['three_d_secure'] ?? [];
            $redirectUrl = $tds['redirect_url'] ?? ($charge['redirect_url'] ?? '');

            if (! empty($redirectUrl)) {
                // Caller (PaymentController::handleCreditCard3D) only renders the
                // 3D-secure view for success/redirect/3d_redirect — 'pending'
                // was silently rejected, so the customer never reached the ACS.
                return [
                    'status' => '3d_redirect',
                    'message' => __('admin/modules.pinpayments_3ds_redirect'),
                    'action' => 'redirect',
                    'redirect_url' => $redirectUrl,
                    'data' => [
                        'charge_token' => $charge['token'] ?? '',
                        'redirect_url' => $redirectUrl,
                    ],
                ];
            }
        }

        return $this->parseChargeResponse($response);
    }

    /**
     * Normalize a POST /charges response into the standard envelope.
     * Reads canonical `success` boolean + `status_message` (ref: Charges API).
     */
    private function parseChargeResponse(array $response): array
    {
        if ($response['http_code'] === 201 && isset($response['response']['response'])) {
            $charge = $response['response']['response'];
            $succeeded = ($charge['success'] ?? null) === true
                || ($charge['status_message'] ?? '') === 'Success';

            if ($succeeded) {
                return [
                    'status' => 'success',
                    'message' => __('admin/modules.pinpayments_payment_success'),
                    'transaction_id' => $charge['token'] ?? '',
                    'data' => [
                        'charge_token' => $charge['token'] ?? '',
                        'amount' => ($charge['amount'] ?? 0) / 100,
                        'currency' => $charge['currency'] ?? '',
                        'status_message' => $charge['status_message'] ?? '',
                    ],
                ];
            }

            return [
                'status' => 'failed',
                'message' => __('admin/modules.pinpayments_payment_declined'),
                'data' => [
                    'charge_token' => $charge['token'] ?? '',
                    'status_message' => $charge['status_message'] ?? '',
                    'error_message' => $charge['error_message'] ?? '',
                ],
            ];
        }

        return [
            'status' => 'error',
            'message' => __('admin/modules.pinpayments_payment_failed'),
            'data' => $this->parsePinError($response),
        ];
    }

    /**
     * Handle webhook callback - verify signature (fail-closed) and re-fetch charge.
     * Ref: pinpayments.com webhooks — header `Pin-Signature: t=<ts>,v1=<sig>`,
     * signed string = `<timestamp>.<raw_body>`, HMAC-SHA256 with signing key.
     */
    public function handlePaymentCallback(array $params)
    {
        $webhookSecret = $this->getConfig('webhook_endpoint_secret', '');

        // Verify webhook signature if a signing key is configured (fail-closed).
        if (! empty($webhookSecret)) {
            // Controller forwards $request->all() only; pull raw body + header
            // straight from the request so verification actually has material.
            $rawBody = $params['raw_body'] ?? (request()?->getContent() ?? '');
            $sigHeader = $params['webhook_signature']
                ?? $params['pin_signature']
                ?? (request()?->header('Pin-Signature') ?? '');

            [$timestamp, $v1] = $this->parsePinSignature($sigHeader);

            if ($timestamp === null || $v1 === null || $rawBody === '') {
                return [
                    'status' => 'failed',
                    'message' => __('admin/modules.pinpayments_webhook_missing_signature'),
                    'data' => [],
                ];
            }

            // Replay protection: reject stale timestamps (5 min tolerance).
            if (abs(time() - (int) $timestamp) > 300) {
                return [
                    'status' => 'failed',
                    'message' => __('admin/modules.pinpayments_webhook_stale'),
                    'data' => [],
                ];
            }

            $expected = $this->hmac('sha256', $timestamp . '.' . $rawBody, $webhookSecret);

            if (! $this->secureCompare($expected, $v1)) {
                return [
                    'status' => 'failed',
                    'message' => __('admin/modules.pinpayments_webhook_signature_mismatch'),
                    'data' => [],
                ];
            }
        }

        // Re-fetch charge details from Pin (never trust the webhook body for state).
        $chargeToken = $params['charge_token'] ?? $params['token'] ?? null;

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

        $response = $this->callPinApi('/charges/' . $chargeToken, 'GET');

        if ($response['http_code'] === 200 && isset($response['response']['response'])) {
            $charge = $response['response']['response'];

            if (($charge['status_message'] ?? '') === 'Success') {
                return [
                    'status' => 'success',
                    'message' => __('admin/modules.pinpayments_payment_success'),
                    'transaction_id' => $charge['token'] ?? '',
                    'data' => [
                        'charge_token' => $charge['token'] ?? '',
                        'amount' => ($charge['amount'] ?? 0) / 100,
                        'currency' => $charge['currency'] ?? '',
                        'status_message' => $charge['status_message'] ?? '',
                        'email' => $charge['email'] ?? '',
                    ],
                ];
            }

            return [
                'status' => 'failed',
                'message' => __('admin/modules.pinpayments_payment_declined'),
                'data' => [
                    'charge_token' => $charge['token'] ?? '',
                    'status_message' => $charge['status_message'] ?? '',
                    'error_message' => $charge['error_message'] ?? '',
                ],
            ];
        }

        return [
            'status' => 'failed',
            'message' => __('admin/modules.pinpayments_callback_failed'),
            'data' => $this->parsePinError($response),
        ];
    }

    /**
     * Begin a 3D Secure charge — delegates to makePayment which sends the
     * `three_d_secure` object and returns a redirect action on 202 Accepted.
     * Requires a `3ds_callback_url` (or `callback_url`) in $params.
     */
    public function start3DPayment(array $params)
    {
        if (empty($params['3ds_callback_url']) && empty($params['callback_url'])) {
            return [
                'status' => 'error',
                'message' => __('admin/modules.pinpayments_3ds_callback_required'),
                'data' => [],
            ];
        }

        // Honesty: if the admin explicitly selected 2D, do not silently downgrade
        // a 3D-start into a direct sale — reject so the caller uses makePayment.
        $securityType = strtolower((string) ($params['security_type'] ?? $this->getConfig('security_type', '3d')));
        if (in_array($securityType, ['2d', 'no_3d', 'off'], true)) {
            return [
                'status' => 'error',
                'message' => __('admin/modules.pinpayments_3ds_disabled'),
                'data' => [],
            ];
        }

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

    /**
     * Complete a 3D Secure charge after the customer returns from the ACS.
     * Ref: Charges API — GET /charges/verify?session_token=<token>.
     */
    public function complete3DPayment(array $params)
    {
        $sessionToken = $params['session_token']
            ?? $params['pin_response_handshake_token']
            ?? (request()?->query('session_token'))
            ?? null;

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

        $response = $this->callPinApi('/charges/verify?session_token=' . urlencode($sessionToken), 'GET');

        return $this->parseChargeResponse($response);
    }

    /**
     * Refund a charge via Pin Payments Refunds API
     */
    public function refundPayment(array $params)
    {
        $chargeToken = $params['transaction_id'] ?? $params['charge_token'] ?? null;

        if (! $chargeToken) {
            return [
                'status' => 'error',
                'message' => __('admin/modules.pinpayments_missing_charge_token'),
                'data' => [],
            ];
        }

        $refundData = [];

        // Amount is optional; if provided, partial refund in cents
        if (isset($params['amount'])) {
            $refundData['amount'] = (int) round($params['amount'] * 100);
        }

        $response = $this->callPinApi('/charges/' . $chargeToken . '/refunds', 'POST', $refundData);

        if ($response['http_code'] === 201 && isset($response['response']['response'])) {
            $refund = $response['response']['response'];

            return [
                'status' => 'success',
                'message' => __('admin/modules.pinpayments_refund_success'),
                'data' => [
                    'refund_token' => $refund['token'] ?? '',
                    'charge_token' => $refund['charge'] ?? $chargeToken,
                    'amount' => ($refund['amount'] ?? 0) / 100,
                    'currency' => $refund['currency'] ?? '',
                    'status_message' => $refund['status_message'] ?? '',
                ],
            ];
        }

        return [
            'status' => 'error',
            'message' => __('admin/modules.pinpayments_refund_failed'),
            'data' => $this->parsePinError($response),
        ];
    }

    /**
     * Payment link generation is not natively supported by Pin Payments
     */
    public function generatePaymentLink(array $params)
    {
        return [
            'status' => 'error',
            'message' => __('admin/modules.pinpayments_payment_link_not_supported'),
        ];
    }

    /**
     * Save a card by creating a customer with a card token
     */
    public function saveCard(array $params)
    {
        if (empty($params['email'])) {
            return [
                'status' => 'error',
                'message' => __('admin/modules.pinpayments_email_required'),
                'data' => [],
            ];
        }

        if (empty($params['card_token'])) {
            return [
                'status' => 'error',
                'message' => __('admin/modules.pinpayments_card_token_required'),
                'data' => [],
            ];
        }

        $customerData = [
            'email' => $params['email'],
            'card_token' => $params['card_token'],
        ];

        if (! empty($params['first_name'])) {
            $customerData['first_name'] = $params['first_name'];
        }

        if (! empty($params['last_name'])) {
            $customerData['last_name'] = $params['last_name'];
        }

        $response = $this->callPinApi('/customers', 'POST', $customerData);

        if ($response['http_code'] === 201 && isset($response['response']['response'])) {
            $customer = $response['response']['response'];

            return [
                'status' => 'success',
                'message' => __('admin/modules.pinpayments_card_saved'),
                'data' => [
                    'customer_token' => $customer['token'] ?? '',
                    'email' => $customer['email'] ?? '',
                    'card_token' => $customer['card']['token'] ?? '',
                    'card_display' => $customer['card']['display_number'] ?? '',
                    'card_scheme' => $customer['card']['scheme'] ?? '',
                ],
            ];
        }

        return [
            'status' => 'error',
            'message' => __('admin/modules.pinpayments_card_save_failed'),
            'data' => $this->parsePinError($response),
        ];
    }

    /**
     * Delete a customer (and their saved card) via Pin Payments API
     */
    public function deleteCard(array $params)
    {
        $customerToken = $params['customer_token'] ?? $params['token'] ?? null;

        if (! $customerToken) {
            return [
                'status' => 'error',
                'message' => __('admin/modules.pinpayments_missing_customer_token'),
                'data' => [],
            ];
        }

        $response = $this->callPinApi('/customers/' . $customerToken, 'DELETE');

        if (in_array($response['http_code'], [200, 204])) {
            return [
                'status' => 'success',
                'message' => __('admin/modules.pinpayments_card_deleted'),
            ];
        }

        return [
            'status' => 'error',
            'message' => __('admin/modules.pinpayments_card_delete_failed'),
            'data' => $this->parsePinError($response),
        ];
    }

    /**
     * Pay with a saved customer token
     */
    public function payWithSavedCard(array $params)
    {
        if (empty($params['customer_token'])) {
            return [
                'status' => 'error',
                'message' => __('admin/modules.pinpayments_missing_customer_token'),
                'data' => [],
            ];
        }

        $currency = strtoupper($params['currency'] ?? $this->getConfig('currency', 'AUD'));

        $chargeData = [
            'email' => $params['email'] ?? '',
            'description' => $params['description'] ?? __('admin/modules.pinpayments_charge_description', ['order_id' => $params['order_id'] ?? '']),
            'amount' => (int) round($params['amount'] * 100),
            'currency' => $currency,
            'ip_address' => $params['ip_address'] ?? request()->ip(),
            'customer_token' => $params['customer_token'],
        ];

        $response = $this->callPinApi('/charges', 'POST', $chargeData);

        return $this->parseChargeResponse($response);
    }

    /**
     * Test API connection by fetching a single charge
     */
    public function testConnection(array $params = [])
    {
        $response = $this->callPinApi('/charges?per_page=1', 'GET');

        if ($response['http_code'] === 200 && isset($response['response'])) {
            return [
                'status' => 'success',
                'message' => __('admin/modules.pinpayments_connection_success'),
            ];
        }

        if ($response['http_code'] === 401 || $response['http_code'] === 403) {
            return [
                'status' => 'error',
                'message' => __('admin/modules.pinpayments_connection_auth_failed'),
                'data' => [
                    'http_code' => $response['http_code'],
                ],
            ];
        }

        return [
            'status' => 'error',
            'message' => __('admin/modules.pinpayments_connection_failed'),
            'data' => $this->parsePinError($response),
        ];
    }

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

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

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

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

    /**
     * Return supported features
     */
    public function getFeatures(): array
    {
        return [
            'refund' => true,
            '3d_secure' => true,
            '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(), [
            'secret_key' => [
                'type' => 'password',
                'label' => 'Secret Key',
                'description' => 'Pin Payments secret API key',
                'required' => true,
                'value' => '',
                'placeholder' => 'Enter your secret key',
            ],
            'publishable_key' => [
                'type' => 'text',
                'label' => 'Publishable Key',
                'description' => 'Pin Payments publishable API key (used in frontend)',
                'required' => true,
                'value' => '',
                'placeholder' => 'Enter your publishable key',
            ],
            'webhook_endpoint_secret' => [
                'type' => 'password',
                'label' => 'Webhook Endpoint Secret',
                'description' => 'Secret for verifying webhook signatures (HMAC-SHA256)',
                'required' => false,
                'value' => '',
                'placeholder' => 'Enter webhook endpoint secret',
            ],
            'currency' => [
                'type' => 'text',
                'label' => 'Default Currency',
                'description' => 'Default settlement currency (ISO 4217, e.g. AUD, USD, NZD, GBP, EUR)',
                'required' => false,
                'value' => 'AUD',
                'placeholder' => 'AUD',
            ],
        ]);
    }

    /**
     * Override default headers with Basic Auth using secret key
     */
    protected function getDefaultHeaders()
    {
        $credentials = base64_encode($this->getConfig('secret_key', '') . ':');

        return [
            'Content-Type: application/json',
            'Accept: application/json',
            'User-Agent: ProCP/1.0 Payment Gateway Module',
            'Authorization: Basic ' . $credentials,
        ];
    }

    /**
     * Get API URL based on test/live mode
     */
    protected function getApiUrl(): string
    {
        return $this->isTestMode()
            ? 'https://test-api.pinpayments.com/1'
            : 'https://api.pinpayments.com/1';
    }

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

    /**
     * Send API request to Pin Payments with Basic Auth.
     * POST → PRC-888 postJson (explicit TLS-verify + timeout + proto restriction);
     * GET/DELETE → hardened curl with TLS-verify + HTTP(S)-only protocols.
     */
    private function callPinApi(string $endpoint, string $method = 'POST', array $data = []): array
    {
        $url = $this->apiUrl . $endpoint;

        if ($method === 'POST') {
            $result = $this->postJson($url, $data, $this->getDefaultHeaders());

            if (! empty($result['error'])) {
                return [
                    'http_code' => 0,
                    'response' => null,
                    'raw_response' => $result['error'],
                    'error' => $result['error'],
                ];
            }

            return [
                'http_code' => $result['http_code'],
                'response' => $result['response'],
                'raw_response' => $result['raw_response'],
            ];
        }

        // GET / DELETE — hardened curl (TLS-verify ON + HTTP(S)-only).
        $ch = curl_init();
        curl_setopt_array($ch, [
            CURLOPT_URL             => $url,
            CURLOPT_RETURNTRANSFER  => true,
            CURLOPT_CUSTOMREQUEST   => $method,
            CURLOPT_HTTPHEADER      => $this->getDefaultHeaders(),
            CURLOPT_TIMEOUT         => 30,
            CURLOPT_CONNECTTIMEOUT  => 10,
            CURLOPT_SSL_VERIFYPEER  => true,
            CURLOPT_SSL_VERIFYHOST  => 2,
            CURLOPT_PROTOCOLS       => CURLPROTO_HTTPS | CURLPROTO_HTTP,
            CURLOPT_REDIR_PROTOCOLS => CURLPROTO_HTTPS | CURLPROTO_HTTP,
        ]);

        $response = curl_exec($ch);
        $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        $curlError = curl_error($ch);
        curl_close($ch);

        if ($curlError) {
            return [
                'http_code' => 0,
                'response' => null,
                'raw_response' => $curlError,
                'error' => $curlError,
            ];
        }

        return [
            'http_code' => $httpCode,
            'response' => json_decode($response, true),
            'raw_response' => $response,
        ];
    }

    /**
     * Parse a `Pin-Signature: t=<ts>,v1=<sig>` header into [timestamp, v1].
     * Returns [null, null] when malformed. Ref: pinpayments.com webhooks.
     */
    private function parsePinSignature(string $header): array
    {
        if ($header === '') {
            return [null, null];
        }

        $timestamp = null;
        $v1 = null;

        foreach (explode(',', $header) as $part) {
            $kv = explode('=', trim($part), 2);
            if (count($kv) !== 2) {
                continue;
            }
            [$key, $value] = $kv;
            $key = trim($key);
            $value = trim($value);
            if ($key === 't') {
                $timestamp = $value;
            } elseif ($key === 'v1') {
                $v1 = $value;
            }
        }

        return [$timestamp, $v1];
    }

    /**
     * Parse error information from Pin Payments API response
     */
    private function parsePinError(array $response): array
    {
        if (isset($response['error'])) {
            return [
                'error_code' => 'CURL_ERROR',
                'error_text' => $response['error'],
            ];
        }

        if (isset($response['response']['error'])) {
            return [
                'error_code' => $response['response']['error_description'] ?? 'API_ERROR',
                'error_text' => $response['response']['error'] ?? __('admin/modules.pinpayments_unknown_error'),
            ];
        }

        if (isset($response['response']['messages'])) {
            $messages = $response['response']['messages'];
            $errorTexts = [];

            foreach ($messages as $message) {
                $errorTexts[] = ($message['param'] ?? '') . ': ' . ($message['message'] ?? '');
            }

            return [
                'error_code' => $response['response']['error_description'] ?? 'VALIDATION_ERROR',
                'error_text' => implode('; ', $errorTexts),
            ];
        }

        return [
            'error_code' => 'UNKNOWN',
            'error_text' => __('admin/modules.pinpayments_unknown_error'),
        ];
    }
}
