<?php

namespace App\Modules\Payment\Conekta;

use App\Core\Payment\AbstractPayment;

class Conekta extends AbstractPayment
{
    protected $apiUrl;

    public function __construct(array $config = [])
    {
        parent::__construct($config);
        $this->apiUrl = 'https://api.conekta.io';
    }

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

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

        if (strtoupper($params['currency']) !== 'MXN') {
            return [
                'status' => 'error',
                'message' => __('admin/modules.conekta_invalid_currency'),
                'data' => [],
            ];
        }

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

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

    /**
     * Create an order via Conekta Orders API
     * Amount is sent in centavos (100 centavos = 1 MXN)
     * Ref: api.conekta.io/orders (HostBill Conekta\Order::create — class.conekta.php:88-94)
     */
    public function makePayment(array $params)
    {
        $currency = strtoupper($params['currency'] ?? 'MXN');
        $amountInCentavos = (int) ($params['amount'] * 100);

        $paymentType = $params['payment_type'] ?? 'card';
        $tokenId = $params['token_id'] ?? '';

        // Card charge requires a tokenized card (Conekta token_id); PAN/CVV never touched (PCI).
        if ($paymentType === 'card' && $tokenId === '') {
            return [
                'status' => 'error',
                'message' => __('admin/modules.conekta_missing_token'),
                'data' => [],
            ];
        }

        $paymentMethod = ['type' => $paymentType];
        if ($tokenId !== '') {
            $paymentMethod['token_id'] = $tokenId;
        }

        $orderData = [
            'currency' => $currency,
            'customer_info' => [
                'name' => $params['customer']['name'] ?? '',
                'email' => $params['customer']['email'] ?? '',
                'phone' => $params['customer']['phone'] ?? '',
            ],
            'line_items' => [
                [
                    'name' => $params['description'] ?? __('admin/modules.conekta_order_description', ['order_id' => $params['order_id'] ?? '']),
                    'unit_price' => $amountInCentavos,
                    'quantity' => $params['quantity'] ?? 1,
                ],
            ],
            'charges' => [
                ['payment_method' => $paymentMethod],
            ],
        ];

        if (isset($params['metadata'])) {
            $orderData['metadata'] = $params['metadata'];
        }

        $response = $this->request($this->apiUrl . '/orders', 'POST', $orderData);

        if ($this->isSuccessHttp($response['http_code']) && isset($response['response']['id'])) {
            $order = $response['response'];
            $mappedStatus = $this->mapConektaStatus($order['payment_status'] ?? '');

            return [
                'status' => $mappedStatus,
                'message' => __('admin/modules.conekta_payment_status_' . $mappedStatus),
                'transaction_id' => $order['id'],
                'data' => [
                    'conekta_order_id' => $order['id'],
                    'payment_status' => $order['payment_status'] ?? '',
                    'amount' => $amountInCentavos / 100,
                    'currency' => $order['currency'] ?? $currency,
                ],
            ];
        }

        return [
            'status' => 'error',
            'message' => $this->extractError($response, 'conekta_payment_failed'),
            'data' => $this->sanitizeErrorData($response),
        ];
    }

    /**
     * Handle webhook callback from Conekta — FAIL-CLOSED.
     *
     * Conekta signs the webhook with an asymmetric RSA key pair: the raw request
     * body is SHA256-signed with Conekta's PRIVATE key; the base64 signature is
     * carried in the `Digest` header and verified with the merchant's PUBLIC key
     * via openssl_verify (developers.conekta.com — autenticación-webhooks).
     *
     * When `webhook_public_key` is configured, signature verification is REQUIRED:
     * a missing/unverifiable signature is REJECTED (never treated as paid). The
     * subsequent order re-fetch is defense-in-depth, not the primary gate.
     */
    public function handlePaymentCallback(array $params)
    {
        $publicKey = (string) $this->getConfig('webhook_public_key', '');

        if ($publicKey !== '') {
            $rawBody = $this->resolveRawPayload($params);
            $digest  = $this->resolveDigest($params);

            // fail-closed: no raw body OR no digest OR verify fails → reject.
            if ($rawBody === '' || $digest === '' || ! $this->verifyWebhook($rawBody, $digest, $publicKey)) {
                return [
                    'status' => 'failed',
                    'message' => __('admin/modules.conekta_webhook_signature_invalid'),
                    'data' => [],
                ];
            }
        }

        $orderId = $params['order_id'] ?? $params['conekta_order_id'] ?? null;
        // Conekta webhook payload nests the order under data.object.id.
        if (! $orderId && isset($params['data']['object']['id'])) {
            $orderId = $params['data']['object']['id'];
        }

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

        // Re-fetch order from Conekta (source of truth, defense-in-depth).
        $response = $this->request($this->apiUrl . '/orders/' . $orderId, 'GET');

        if ($this->isSuccessHttp($response['http_code']) && isset($response['response']['id'])) {
            $order = $response['response'];
            $mappedStatus = $this->mapConektaStatus($order['payment_status'] ?? '');

            return [
                'status' => $mappedStatus,
                'message' => __('admin/modules.conekta_payment_status_' . $mappedStatus),
                'transaction_id' => $order['id'],
                'data' => [
                    'conekta_order_id' => $order['id'],
                    'payment_status' => $order['payment_status'] ?? '',
                    'amount' => ($order['amount'] ?? 0) / 100,
                    'currency' => $order['currency'] ?? '',
                ],
            ];
        }

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

    /**
     * Resolve the RAW (unparsed) webhook body from the callback params.
     * The controller forwards $request->all() (parsed); the raw body is only
     * available if the caller supplies it explicitly. Absent → '' (fail-closed).
     */
    private function resolveRawPayload(array $params): string
    {
        foreach (['raw_payload', 'raw_body', 'payload'] as $key) {
            if (isset($params[$key]) && is_string($params[$key]) && $params[$key] !== '') {
                return $params[$key];
            }
        }

        return '';
    }

    /**
     * Resolve the base64 `Digest` header value from the callback params.
     * Absent → '' (fail-closed).
     */
    private function resolveDigest(array $params): string
    {
        foreach (['digest', 'Digest', 'signature'] as $key) {
            if (isset($params[$key]) && is_string($params[$key]) && $params[$key] !== '') {
                return $params[$key];
            }
        }

        return '';
    }

    /**
     * Refund a payment via Conekta Refunds API
     */
    public function refundPayment(array $params)
    {
        $orderId = $params['transaction_id'] ?? $params['conekta_order_id'] ?? null;

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

        $refundData = [
            'reason' => $params['reason'] ?? 'requested_by_client',
        ];

        if (isset($params['amount'])) {
            $refundData['amount'] = (int) ($params['amount'] * 100);
        }

        // Ref: POST /orders/{id}/refunds — HostBill $order->refund(reason,amount) (class.conekta.php:138).
        $response = $this->request(
            $this->apiUrl . '/orders/' . $orderId . '/refunds',
            'POST',
            $refundData
        );

        // Conekta returns the ORDER with payment_status refunded/partially_refunded;
        // accept either a refund id or a confirmed refunded status (HostBill:152).
        $body = $response['response'] ?? [];
        $refundedStatus = in_array($body['payment_status'] ?? '', ['refunded', 'partially_refunded'], true);

        if ($this->isSuccessHttp($response['http_code']) && (isset($body['id']) || $refundedStatus)) {
            return [
                'status' => 'success',
                'message' => __('admin/modules.conekta_refund_success'),
                'data' => [
                    'refund_id' => $body['id'] ?? $orderId,
                    'order_id' => $orderId,
                    'amount' => ($body['amount'] ?? 0) / 100,
                    'currency' => $body['currency'] ?? '',
                    'payment_status' => $body['payment_status'] ?? '',
                ],
            ];
        }

        return [
            'status' => 'error',
            'message' => $this->extractError($response, 'conekta_refund_failed'),
            'data' => $this->sanitizeErrorData($response),
        ];
    }

    /**
     * Generate a hosted-checkout (Payment Link) via Conekta Checkouts API.
     *
     * Ref: POST api.conekta.io/checkouts — HostedPayment integration expects
     * {name, type:'HostedPayment', recurrent, expires_at, allowed_payment_methods,
     * order_template:{currency, customer_info, line_items}} (developers.conekta.com
     * — checkouts). Flat top-level line_items is rejected (422).
     */
    public function generatePaymentLink(array $params)
    {
        $currency = strtoupper($params['currency'] ?? 'MXN');
        $amountInCentavos = (int) ($params['amount'] * 100);
        $allowedMethods = $params['allowed_payment_methods'] ?? ['card', 'cash', 'bank_transfer'];

        // Default expiry 24h from now if caller does not provide one (unix seconds).
        $expiresAt = isset($params['expires_at']) ? (int) $params['expires_at'] : (time() + 86400);

        $checkoutData = [
            'name' => $params['name'] ?? __('admin/modules.conekta_order_description', ['order_id' => $params['order_id'] ?? '']),
            'type' => 'HostedPayment',
            'recurrent' => false,
            'expires_at' => $expiresAt,
            'allowed_payment_methods' => $allowedMethods,
            'order_template' => [
                'currency' => $currency,
                'customer_info' => [
                    'name' => $params['customer']['name'] ?? '',
                    'email' => $params['customer']['email'] ?? '',
                    'phone' => $params['customer']['phone'] ?? '',
                ],
                'line_items' => [
                    [
                        'name' => $params['description'] ?? __('admin/modules.conekta_order_description', ['order_id' => $params['order_id'] ?? '']),
                        'unit_price' => $amountInCentavos,
                        'quantity' => $params['quantity'] ?? 1,
                    ],
                ],
            ],
        ];

        if (isset($params['metadata'])) {
            $checkoutData['order_template']['metadata'] = $params['metadata'];
        }

        $response = $this->request($this->apiUrl . '/checkouts', 'POST', $checkoutData);

        if ($this->isSuccessHttp($response['http_code']) && isset($response['response']['id'])) {
            return [
                'status' => 'success',
                'message' => __('admin/modules.conekta_payment_link_created'),
                'data' => [
                    'checkout_id' => $response['response']['id'],
                    'payment_url' => $response['response']['url'] ?? '',
                    'amount' => $amountInCentavos / 100,
                    'allowed_payment_methods' => $allowedMethods,
                ],
            ];
        }

        return [
            'status' => 'error',
            'message' => $this->extractError($response, 'conekta_payment_link_failed'),
            'data' => $this->sanitizeErrorData($response),
        ];
    }

    /**
     * Test API connection by fetching customers
     */
    public function testConnection(array $params = [])
    {
        $response = $this->request($this->apiUrl . '/customers?limit=1', 'GET');

        if ($this->isSuccessHttp($response['http_code'])) {
            return [
                'status' => 'success',
                'message' => __('admin/modules.conekta_connection_success'),
                'data' => [],
            ];
        }

        return [
            'status' => 'error',
            'message' => $this->extractError($response, 'conekta_connection_failed'),
            'data' => $this->sanitizeErrorData($response),
        ];
    }

    /**
     * Validate module configuration
     */
    public function validateConfig()
    {
        if (empty($this->getConfig('private_key'))) {
            return [
                'status' => 'error',
                'message' => __('admin/modules.conekta_private_key_required'),
            ];
        }

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

    /**
     * Supported features
     */
    public function getFeatures(): array
    {
        return [
            'refund' => true,
            '3d_secure' => false,
            'save_card' => false,
            'recurring' => false,
            'fraud_check' => true,
            'installment' => 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(), [
            'private_key' => [
                'type' => 'password',
                'label' => 'Private Key',
                'description' => 'Conekta private API key (starts with key_)',
                'required' => true,
                'value' => '',
                'placeholder' => 'key_...',
            ],
            'public_key' => [
                'type' => 'text',
                'label' => 'Public Key',
                'description' => 'Conekta public API key (starts with key_)',
                'required' => true,
                'value' => '',
                'placeholder' => 'key_...',
            ],
            'webhook_public_key' => [
                'type' => 'textarea',
                'label' => 'Webhook Public Key (PEM)',
                'description' => 'Conekta webhook RSA public key (PEM) for asymmetric Digest signature verification. When set, webhook signatures are required (fail-closed).',
                'required' => false,
                'value' => '',
                'placeholder' => "-----BEGIN PUBLIC KEY-----\n...\n-----END PUBLIC KEY-----",
            ],
        ]);
    }

    /**
     * Override default headers for Conekta Basic Auth
     * Uses private_key as username with empty password
     */
    protected function getDefaultHeaders()
    {
        $credentials = base64_encode($this->getConfig('private_key', '') . ':');

        return [
            'Content-Type: application/json',
            'Accept: application/vnd.conekta-v2.1.0+json',
            'User-Agent: ProCP/1.0 Conekta-Client-User-Agent',
            'Authorization: Basic ' . $credentials,
        ];
    }

    /**
     * Verify a Conekta webhook signature (asymmetric RSA, fail-closed).
     *
     * Conekta signs the raw body with its PRIVATE key; the base64 `Digest` header
     * is verified with the merchant PUBLIC key via openssl_verify + SHA256
     * (developers.conekta.com — autenticación-webhooks). Any empty input, a bad
     * base64 digest, an unloadable key, or a non-1 verify result → false.
     *
     * @param  string  $payload       Raw (unparsed) request body, UTF-8.
     * @param  string  $signature     Base64-encoded `Digest` header value.
     * @param  string  $publicKeyPem  RSA public key in PEM format.
     */
    public function verifyWebhook(string $payload, string $signature, string $publicKeyPem): bool
    {
        if ($payload === '' || $signature === '' || trim($publicKeyPem) === '') {
            return false;
        }

        $binarySignature = base64_decode($signature, true);
        if ($binarySignature === false || $binarySignature === '') {
            return false;
        }

        $publicKey = openssl_pkey_get_public($publicKeyPem);
        if ($publicKey === false) {
            return false;
        }

        $result = openssl_verify($payload, $binarySignature, $publicKey, OPENSSL_ALGO_SHA256);

        return $result === 1;
    }

    /**
     * Conekta-specific JSON transport (PRC-888). Delegates to postJson for
     * GET/POST with TLS peer/host verification + timeouts; global callApi() is
     * NOT modified (would affect other gateways).
     */
    private function request(string $url, string $method = 'POST', array $data = []): array
    {
        $headers = $this->getDefaultHeaders();

        if (strtoupper($method) === 'GET') {
            // postJson always POSTs; GET reads use a verified curl with the same
            // header set. Reuse postJson's TLS/timeout policy via a null body POST
            // is not valid for reads, so issue a verified GET here.
            return $this->getJson($url, $headers);
        }

        return $this->postJson($url, $data, $headers);
    }

    /** Verified GET (TLS peer/host verify + timeout), JSON-decoded. */
    private function getJson(string $url, array $headers = []): array
    {
        $ch = curl_init();
        curl_setopt_array($ch, [
            CURLOPT_URL            => $url,
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_HTTPGET        => true,
            CURLOPT_HTTPHEADER     => $headers,
            CURLOPT_SSL_VERIFYPEER => true,
            CURLOPT_SSL_VERIFYHOST => 2,
            CURLOPT_TIMEOUT        => 30,
            CURLOPT_CONNECTTIMEOUT => 10,
            CURLOPT_PROTOCOLS       => CURLPROTO_HTTPS | CURLPROTO_HTTP,
            CURLOPT_REDIR_PROTOCOLS => CURLPROTO_HTTPS | CURLPROTO_HTTP,
        ]);

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

        return [
            'http_code'    => $httpCode,
            'raw_response' => $response === false ? '' : (string) $response,
            'response'     => json_decode((string) $response, true),
            'error'        => $error !== '' ? $error : null,
        ];
    }

    /** Conekta Orders/Refunds create returns 200; some v2.1 endpoints 201. */
    private function isSuccessHttp(int $httpCode): bool
    {
        return in_array($httpCode, [200, 201], true);
    }

    /** Extract a human error message from a Conekta error body (never raw secrets). */
    private function extractError(array $response, string $fallbackKey): string
    {
        return $response['response']['details'][0]['message']
            ?? $response['response']['message']
            ?? __('admin/modules.' . $fallbackKey);
    }

    /**
     * Strip raw_response (may carry card token/last4/PII — PCI) from error data;
     * return only the http_code so the caller/logs never see the raw body.
     */
    private function sanitizeErrorData(array $response): array
    {
        return ['http_code' => $response['http_code'] ?? 0];
    }

    /**
     * Map Conekta payment status to internal status
     *
     * Conekta statuses: paid, pending_payment, declined, expired
     * Internal statuses: success, pending, failed
     */
    private function mapConektaStatus(string $conektaStatus): string
    {
        return match ($conektaStatus) {
            'paid' => 'success',
            'pending_payment' => 'pending',
            'declined' => 'failed',
            'expired' => 'failed',
            default => 'failed',
        };
    }
}
