<?php

namespace App\Services\Payment\Drivers;

use App\Models\Payment;
use App\Services\Payment\AbstractGatewayDriver;
use App\Services\Payment\PaymentResult;
use Illuminate\Support\Facades\Log;

/**
 * PayPal payment gateway driver.
 *
 * Uses the PayPal REST API v2 (Orders API) via cURL.
 * Creates an order, redirects to PayPal for approval, then captures on callback.
 *
 * @see https://developer.paypal.com/docs/api/orders/v2/
 */
class PayPalDriver extends AbstractGatewayDriver
{
    private const SANDBOX_BASE = 'https://api-m.sandbox.paypal.com';
    private const LIVE_BASE    = 'https://api-m.paypal.com';

    public static function credentialFields(): array
    {
        return [
            ['key' => 'client_id',     'label' => 'Client ID',     'type' => 'text',     'required' => true],
            ['key' => 'client_secret', 'label' => 'Client Secret', 'type' => 'password', 'required' => true],
            ['key' => 'webhook_id',    'label' => 'Webhook ID',    'type' => 'text',     'required' => false,
             'hint' => 'Found in PayPal Developer Dashboard under Webhooks. Used for signature verification.'],
        ];
    }

    public function initiate(Payment $payment, string $callbackUrl): array
    {
        // [DIAG] Stamp the very start of the flow so we can correlate every
        // log line that follows with this specific payment attempt.
        Log::info('PayPal.initiate: start', [
            'payment_id'   => $payment->id,
            'amount'       => $payment->amount,
            'currency'     => strtoupper($payment->currency ?? 'USD'),
            'mode'         => $this->isSandbox() ? 'sandbox' : 'live',
            'base_url'     => $this->baseUrl(),
            'callback_url' => $callbackUrl,
        ]);

        $accessToken = $this->getAccessToken();

        $payload = [
            'intent' => 'CAPTURE',
            'purchase_units' => [
                [
                    'reference_id' => (string) $payment->id,
                    'description'  => $this->paymentDescription($payment),
                    'amount' => [
                        'currency_code' => strtoupper($payment->currency ?? 'USD'),
                        'value'         => number_format((float) $payment->amount, 2, '.', ''),
                    ],
                ],
            ],
            'payment_source' => [
                'paypal' => [
                    'experience_context' => [
                        'return_url'                 => $callbackUrl . '?success=1',
                        'cancel_url'                 => $callbackUrl . '?cancelled=1',
                        'brand_name'                 => config('app.name'),
                        'user_action'                => 'PAY_NOW',
                        'payment_method_preference'  => 'IMMEDIATE_PAYMENT_REQUIRED',
                    ],
                ],
            ],
        ];

        // [DIAG] Log the outbound payload exactly as PayPal will see it.
        // No PII concerns here — currency, amount, brand name only.
        Log::debug('PayPal.initiate: order payload', [
            'payment_id' => $payment->id,
            'endpoint'   => $this->baseUrl() . '/v2/checkout/orders',
            'payload'    => $payload,
        ]);

        $response = $this->httpRequest('POST', $this->baseUrl() . '/v2/checkout/orders', $payload, [
            'Authorization: Bearer ' . $accessToken,
        ]);

        $json = $response['json'] ?? [];

        if (isset($json['id'])) {
            // PayPal's Orders v2 returns the buyer-redirect URL under one
            // of two `rel` values depending on the request shape:
            //   - rel="approve"        → classic flow without an explicit
            //                            payment_source.paypal.experience_context
            //   - rel="payer-action"   → modern flow WHEN we send the
            //                            payment_source.paypal.experience_context
            //                            block (which we do). This is what
            //                            the live log showed.
            // We accept either so the driver works whether the merchant's
            // PayPal account responds with the old or new shape. Without
            // this check the order is created upstream but we throw a
            // "Failed to create PayPal order." error and never redirect
            // the buyer, leaving an orphan PAYER_ACTION_REQUIRED order
            // dangling on PayPal's side.
            $links = collect($json['links'] ?? []);
            $approveUrl = $links->firstWhere('rel', 'payer-action')['href']
                ?? $links->firstWhere('rel', 'approve')['href']
                ?? null;

            if ($approveUrl) {
                // [DIAG] Order created — log the id PayPal assigned so we can
                // cross-reference with their Activity dashboard when needed.
                Log::info('PayPal.initiate: order created', [
                    'payment_id'      => $payment->id,
                    'paypal_order_id' => $json['id'],
                    'status'          => $json['status'] ?? null,
                    'redirect_url'    => $approveUrl,
                ]);
                return ['redirect_url' => $approveUrl];
            }

            // [DIAG] Edge case — PayPal accepted the order but didn't give us
            // a payer-redirect link under either rel. Log the full link set
            // so we can see what PayPal actually returned (HATEOAS shape
            // varies by account / product activation).
            Log::warning('PayPal.initiate: order id present but no payer redirect link', [
                'payment_id' => $payment->id,
                'paypal_id'  => $json['id'],
                'links'      => $json['links'] ?? [],
            ]);
        }

        // [DIAG] CRITICAL — this is the path that produces the vague
        // "Failed to create PayPal order." error. Log the raw HTTP status,
        // headers (if available), and the FULL body so the next failed
        // payment leaves a complete trail for diagnosis. We log at error
        // level so the line jumps out of the noise in storage/logs/.
        Log::error('PayPal.initiate: order creation failed — raw response', [
            'payment_id'    => $payment->id,
            'http_status'   => $response['status'] ?? null,
            'parsed_json'   => $json,
            'raw_body'      => $response['body']   ?? null,
            'request_payload' => $payload,
            'mode'          => $this->isSandbox() ? 'sandbox' : 'live',
            'base_url'      => $this->baseUrl(),
        ]);

        // PayPal returns a structured error envelope:
        //   { name, message, debug_id, details: [{ issue, description, field, ... }] }
        // The generic top-level `message` is opaque ("semantically incorrect,
        // or failed business validation") — the REAL reason is in
        // `details[].issue` / `details[].description`. Surface that so admins
        // can act on it instead of staring at PayPal's catch-all wording.
        throw new \RuntimeException("PayPal: " . $this->formatPayPalError($json));
    }

    /**
     * Build a human-readable error string from PayPal's error envelope.
     * Includes the `debug_id` so PayPal Merchant Support can look up the
     * exact request when a customer reports it.
     */
    private function formatPayPalError(array $json): string
    {
        $top = (string) ($json['message'] ?? 'Failed to create PayPal order.');
        $debugId = $json['debug_id'] ?? null;

        $details = $json['details'] ?? [];
        if (is_array($details) && $details) {
            $reasons = [];
            foreach ($details as $d) {
                $issue = $d['issue'] ?? '';
                $desc  = $d['description'] ?? '';
                $field = $d['field'] ?? '';
                $bits = array_filter([$issue, $desc, $field ? "field={$field}" : null]);
                if ($bits) $reasons[] = implode(' — ', $bits);
            }
            if ($reasons) {
                $top .= ' (' . implode(' | ', $reasons) . ')';
            }
        }

        if ($debugId) {
            $top .= " [debug_id={$debugId}]";
        }

        return $top;
    }

    public function handleCallback(array $payload): PaymentResult
    {
        // [DIAG] Always log the callback payload so we can see what PayPal
        // (or our return_url forward) actually delivered. Useful when the
        // customer "returns" but the capture fails or the order id is
        // missing — common when return_url is hit twice or the visitor
        // refreshes.
        Log::info('PayPal.callback: received', [
            'payload_keys' => array_keys($payload),
            'has_token'    => isset($payload['token']),
            'has_order_id' => isset($payload['order_id']),
            'cancelled'    => $payload['cancelled'] ?? null,
        ]);

        if (isset($payload['cancelled'])) {
            return PaymentResult::failure('Payment was cancelled by the customer.');
        }

        $paypalOrderId = $payload['token'] ?? $payload['order_id'] ?? null;
        if (! $paypalOrderId) {
            Log::warning('PayPal.callback: missing order id in payload', ['payload' => $payload]);
            return PaymentResult::failure('Missing PayPal order ID in callback.');
        }

        $accessToken = $this->getAccessToken();
        $response    = $this->httpRequest(
            'POST',
            $this->baseUrl() . "/v2/checkout/orders/{$paypalOrderId}/capture",
            [],
            ['Authorization: Bearer ' . $accessToken],
        );

        $json   = $response['json'] ?? [];
        $status = $json['status'] ?? '';

        if ($status === 'COMPLETED') {
            $capture       = $json['purchase_units'][0]['payments']['captures'][0] ?? [];
            $transactionId = $capture['id'] ?? $paypalOrderId;

            // [DIAG] Capture succeeded — log the IDs so we can correlate
            // with PayPal's Activity dashboard.
            Log::info('PayPal.callback: capture succeeded', [
                'paypal_order_id'   => $paypalOrderId,
                'paypal_capture_id' => $transactionId,
                'payer_email'       => $json['payer']['email_address'] ?? null,
            ]);

            return PaymentResult::success(
                transactionId: $transactionId,
                message: 'Payment captured successfully.',
                metadata: [
                    'paypal_order_id'   => $paypalOrderId,
                    'paypal_capture_id' => $transactionId,
                    'payer_email'       => $json['payer']['email_address'] ?? null,
                ],
            );
        }

        // [DIAG] Capture failed — log the raw response so the operator sees
        // why (DECLINED / INSTRUMENT_DECLINED / PAYER_ACTION_REQUIRED etc.).
        // The truncated `"Status: {$status}"` we return to the user is
        // useless without the underlying details.
        Log::error('PayPal.callback: capture failed — raw response', [
            'paypal_order_id' => $paypalOrderId,
            'http_status'     => $response['status'] ?? null,
            'paypal_status'   => $status,
            'parsed_json'     => $json,
            'raw_body'        => $response['body']   ?? null,
        ]);

        return PaymentResult::failure("PayPal order not completed. Status: {$status}");
    }

    public function verifyWebhookSignature(string $rawBody, array $headers): bool
    {
        $webhookId = $this->credential('webhook_id');
        if (! $webhookId) {
            // If no webhook_id configured, fall back to verifying the event
            // by fetching it from PayPal API (less efficient but secure)
            return $this->verifyWebhookViaApi($rawBody, $headers);
        }

        // PayPal webhook signature verification via API
        // @see https://developer.paypal.com/api/rest/webhooks/#link-verifywebhooksignature
        $transmissionId   = $headers['paypal-transmission-id'] ?? $headers['PAYPAL-TRANSMISSION-ID'] ?? '';
        $transmissionTime = $headers['paypal-transmission-time'] ?? $headers['PAYPAL-TRANSMISSION-TIME'] ?? '';
        $transmissionSig  = $headers['paypal-transmission-sig'] ?? $headers['PAYPAL-TRANSMISSION-SIG'] ?? '';
        $certUrl          = $headers['paypal-cert-url'] ?? $headers['PAYPAL-CERT-URL'] ?? '';
        $authAlgo         = $headers['paypal-auth-algo'] ?? $headers['PAYPAL-AUTH-ALGO'] ?? 'SHA256withRSA';

        if (! $transmissionId || ! $transmissionTime || ! $transmissionSig || ! $certUrl) {
            Log::warning('PayPal webhook missing signature headers');
            return false;
        }

        try {
            $accessToken = $this->getAccessToken();

            $verifyPayload = [
                'auth_algo'         => $authAlgo,
                'cert_url'          => $certUrl,
                'transmission_id'   => $transmissionId,
                'transmission_sig'  => $transmissionSig,
                'transmission_time' => $transmissionTime,
                'webhook_id'        => $webhookId,
                'webhook_event'     => json_decode($rawBody, true),
            ];

            $response = $this->httpRequest(
                'POST',
                $this->baseUrl() . '/v1/notifications/verify-webhook-signature',
                $verifyPayload,
                ['Authorization: Bearer ' . $accessToken],
            );

            $json = $response['json'] ?? [];
            $verificationStatus = $json['verification_status'] ?? '';

            return $verificationStatus === 'SUCCESS';
        } catch (\Throwable $e) {
            Log::error('PayPal webhook signature verification failed', [
                'error' => $e->getMessage(),
            ]);
            return false;
        }
    }

    /**
     * Fallback: verify a webhook event by fetching it from PayPal API.
     * If the event exists and matches, it's legitimate.
     */
    private function verifyWebhookViaApi(string $rawBody, array $headers): bool
    {
        $payload = json_decode($rawBody, true);
        $eventId = $payload['id'] ?? '';

        if (! $eventId) {
            return false;
        }

        try {
            $accessToken = $this->getAccessToken();
            $response = $this->httpRequest(
                'GET',
                $this->baseUrl() . "/v1/notifications/webhooks-events/{$eventId}",
                [],
                ['Authorization: Bearer ' . $accessToken],
            );

            $json = $response['json'] ?? [];

            // If PayPal returns the event, it's genuine
            return isset($json['id']) && $json['id'] === $eventId;
        } catch (\Throwable $e) {
            Log::error('PayPal webhook event verification failed', [
                'event_id' => $eventId,
                'error' => $e->getMessage(),
            ]);
            return false;
        }
    }

    public function handleWebhook(array $payload): PaymentResult
    {
        $eventType = $payload['event_type'] ?? '';
        $resource  = $payload['resource'] ?? [];

        if ($eventType === 'PAYMENT.CAPTURE.COMPLETED') {
            $captureId = $resource['id'] ?? '';

            if ($captureId) {
                $accessToken = $this->getAccessToken();
                $response = $this->httpRequest(
                    'GET',
                    $this->baseUrl() . "/v2/payments/captures/{$captureId}",
                    [],
                    ['Authorization: Bearer ' . $accessToken],
                );

                $json   = $response['json'] ?? [];
                $status = $json['status'] ?? '';

                if ($status === 'COMPLETED') {
                    return PaymentResult::success(
                        transactionId: $captureId,
                        message: 'Webhook confirmed and verified payment capture.',
                        metadata: [
                            'paypal_event_type' => $eventType,
                            'amount'            => $json['amount']['value'] ?? null,
                            'currency'          => $json['amount']['currency_code'] ?? null,
                        ],
                    );
                }

                return PaymentResult::failure("PayPal capture status: {$status}");
            }
        }

        return PaymentResult::failure("Unhandled PayPal webhook event: {$eventType}");
    }

    public function verify(Payment $payment): PaymentResult
    {
        $transactionId = $payment->gateway_transaction_id ?? null;
        if (! $transactionId) {
            return PaymentResult::failure('No transaction ID available for verification.');
        }

        $accessToken = $this->getAccessToken();
        $response = $this->httpRequest(
            'GET',
            $this->baseUrl() . "/v2/payments/captures/{$transactionId}",
            [],
            ['Authorization: Bearer ' . $accessToken],
        );

        $json   = $response['json'] ?? [];
        $status = $json['status'] ?? '';

        if ($status === 'COMPLETED') {
            return PaymentResult::success(
                transactionId: $transactionId,
                message: 'Payment verified successfully.',
            );
        }

        return PaymentResult::failure("Payment not verified. Status: {$status}");
    }

    private function baseUrl(): string
    {
        return $this->isSandbox() ? self::SANDBOX_BASE : self::LIVE_BASE;
    }

    private function getAccessToken(): string
    {
        $clientId     = $this->credential('client_id');
        $clientSecret = $this->credential('client_secret');

        // [DIAG] Verify creds shape WITHOUT leaking the values themselves.
        // PayPal client_id is a public-ish string (visible in Developer
        // dashboard) so the prefix + length are safe. The secret only gets
        // a length + last-4 — enough to confirm "yes the right secret is
        // wired" without exposing it in the log.
        $idPreview     = $clientId
            ? substr($clientId, 0, 8) . '…(' . strlen($clientId) . ')'
            : '(empty)';
        $secretPreview = $clientSecret
            ? '…' . substr($clientSecret, -4) . '(' . strlen($clientSecret) . ')'
            : '(empty)';

        Log::debug('PayPal.token: requesting access token', [
            'mode'             => $this->isSandbox() ? 'sandbox' : 'live',
            'token_endpoint'   => $this->baseUrl() . '/v1/oauth2/token',
            'client_id_preview'     => $idPreview,
            'client_secret_preview' => $secretPreview,
            'client_id_present'     => !empty($clientId),
            'client_secret_present' => !empty($clientSecret),
        ]);

        $ch = curl_init();
        curl_setopt_array($ch, [
            CURLOPT_URL            => $this->baseUrl() . '/v1/oauth2/token',
            CURLOPT_POST           => true,
            CURLOPT_POSTFIELDS     => 'grant_type=client_credentials',
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_TIMEOUT        => 15,
            CURLOPT_USERPWD        => $clientId . ':' . $clientSecret,
            CURLOPT_HTTPHEADER     => ['Accept: application/json', 'Accept-Language: en_US'],
            CURLOPT_SSL_VERIFYPEER => true,
            CURLOPT_SSL_VERIFYHOST => 2,
        ]);

        $body       = curl_exec($ch);
        $error      = curl_error($ch);
        $httpStatus = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
        curl_close($ch);

        if ($body === false) {
            // [DIAG] Network-level failure — DNS, TLS handshake, timeout.
            // Surface curl's error string so the operator can act (e.g.
            // shared host that blocks outbound to api-m.paypal.com).
            Log::error('PayPal.token: cURL transport failure', [
                'curl_error' => $error,
                'endpoint'   => $this->baseUrl() . '/v1/oauth2/token',
            ]);
            throw new \RuntimeException("PayPal token request failed: {$error}");
        }

        $json = json_decode($body, true);

        if (isset($json['access_token'])) {
            // [DIAG] Success — log scope + expiry so we can confirm the
            // token actually carries Orders v2 capability. If `scope` is
            // missing payment scopes, the order creation will fail later
            // with the vague "Failed to create PayPal order." we've been
            // hunting.
            Log::info('PayPal.token: obtained', [
                'mode'       => $this->isSandbox() ? 'sandbox' : 'live',
                'expires_in' => $json['expires_in'] ?? null,
                'app_id'     => $json['app_id']     ?? null,
                'nonce'      => substr($json['nonce'] ?? '', 0, 8),
                'scope'      => $json['scope']      ?? null,
            ]);
            return $json['access_token'];
        }

        // [DIAG] Auth failure — the most common path that ultimately
        // produces "Failed to create PayPal order." Log status + full body
        // so the operator sees PayPal's actual rejection (`invalid_client`,
        // `unauthorized_client`, sandbox-vs-live mismatch, etc.).
        Log::error('PayPal.token: no access_token in response', [
            'http_status' => $httpStatus,
            'parsed_json' => $json,
            'raw_body'    => $body,
            'mode'        => $this->isSandbox() ? 'sandbox' : 'live',
            'endpoint'    => $this->baseUrl() . '/v1/oauth2/token',
        ]);

        throw new \RuntimeException('PayPal: Unable to obtain access token.');
    }
}
