<?php
/**
 * Minimal, self-contained PayPal Checkout (Orders v2) REST client.
 *
 * Security model: the app NEVER trusts the browser to tell it a payment
 * happened. It creates the order server-side, and on the buyer's return it
 * CAPTURES the order server-side and reads PayPal's own response. A submission
 * is only fulfilled when PayPal reports status COMPLETED for the right amount.
 *
 * Credentials live in the paypal_settings table (entered via the admin page);
 * they are never in source control and never sent to the browser.
 */

if (!function_exists('pp_settings')) {

    function pp_settings(PDO $pdo): array
    {
        static $cache = null;
        if ($cache !== null) return $cache;

        // Preferred: credentials typed into paypal_config.php (variables at its top).
        $cfg = __DIR__ . '/paypal_config.php';
        if (is_file($cfg)) {
            $PAYPAL_ENVIRONMENT = $PAYPAL_CLIENT_ID = $PAYPAL_SECRET = $PAYPAL_CURRENCY = null;
            $PAYPAL_ENABLED = null;
            include $cfg;
            if (!empty($PAYPAL_CLIENT_ID) && !empty($PAYPAL_SECRET)) {
                return $cache = [
                    'environment' => (($PAYPAL_ENVIRONMENT ?? 'sandbox') === 'live') ? 'live' : 'sandbox',
                    'client_id'   => (string)$PAYPAL_CLIENT_ID,
                    'secret'      => (string)$PAYPAL_SECRET,
                    'currency'    => strtoupper((string)($PAYPAL_CURRENCY ?? 'USD')),
                    'enabled'     => !empty($PAYPAL_ENABLED) ? '1' : '0',
                ];
            }
        }

        // Fallback: the admin PayPal Settings page (paypal_settings DB table).
        $out = [];
        foreach ($pdo->query("SELECT setting_key, setting_value FROM paypal_settings") as $r) {
            $out[$r['setting_key']] = $r['setting_value'];
        }
        return $cache = $out;
    }

    function pp_is_enabled(PDO $pdo): bool
    {
        $s = pp_settings($pdo);
        return ($s['enabled'] ?? '0') === '1'
            && !empty($s['client_id']) && !empty($s['secret']);
    }

    function pp_api_base(string $env): string
    {
        return $env === 'live'
            ? 'https://api-m.paypal.com'
            : 'https://api-m.sandbox.paypal.com';
    }

    /**
     * Low-level HTTPS call to the PayPal API. Returns [http_status, decoded_body].
     * Throws RuntimeException on transport failure.
     */
    function pp_http(string $method, string $url, array $headers, ?string $body): array
    {
        $ch = curl_init($url);
        curl_setopt_array($ch, [
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_CUSTOMREQUEST  => $method,
            CURLOPT_HTTPHEADER     => $headers,
            CURLOPT_TIMEOUT        => 30,
            CURLOPT_SSL_VERIFYPEER => true,
            CURLOPT_SSL_VERIFYHOST => 2,
        ]);
        if ($body !== null) {
            curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
        }
        $raw    = curl_exec($ch);
        $status = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
        if ($raw === false) {
            $err = curl_error($ch);
            curl_close($ch);
            throw new RuntimeException('PayPal transport error: ' . $err);
        }
        curl_close($ch);
        $decoded = json_decode($raw, true);
        return [$status, is_array($decoded) ? $decoded : []];
    }

    /** OAuth2 client-credentials access token. Cached per request. */
    function pp_token(PDO $pdo): string
    {
        static $token = null;
        if ($token !== null) return $token;

        $s   = pp_settings($pdo);
        $env = $s['environment'] ?? 'sandbox';
        if (empty($s['client_id']) || empty($s['secret'])) {
            throw new RuntimeException('PayPal credentials are not configured.');
        }

        [$status, $body] = pp_http(
            'POST',
            pp_api_base($env) . '/v1/oauth2/token',
            [
                'Authorization: Basic ' . base64_encode($s['client_id'] . ':' . $s['secret']),
                'Content-Type: application/x-www-form-urlencoded',
            ],
            'grant_type=client_credentials'
        );

        if ($status !== 200 || empty($body['access_token'])) {
            throw new RuntimeException('PayPal auth failed (HTTP ' . $status . '): ' . ($body['error_description'] ?? 'unknown'));
        }
        return $token = $body['access_token'];
    }

    /**
     * Create a CAPTURE order. Returns ['id' => orderId, 'approve' => approvalUrl].
     * $reference is our own pending-row id, echoed back as custom_id.
     */
    function pp_create_order(PDO $pdo, float $amount, string $currency, string $reference, string $return_url, string $cancel_url, string $description = 'Contest entry'): array
    {
        $s   = pp_settings($pdo);
        $env = $s['environment'] ?? 'sandbox';

        // PayPal caps description at 127 chars.
        $description = mb_substr($description, 0, 127);

        $payload = json_encode([
            'intent' => 'CAPTURE',
            'purchase_units' => [[
                'custom_id' => $reference,
                'description' => $description,
                'amount' => [
                    'currency_code' => $currency,
                    'value' => number_format($amount, 2, '.', ''),
                ],
            ]],
            'application_context' => [
                // Shown on the PayPal checkout page; falls back to a neutral
                // label if the site name isn't in scope for some reason.
                'brand_name'  => (string)($GLOBALS['site_name'] ?? 'Submission Manager'),
                'user_action' => 'PAY_NOW',
                'shipping_preference' => 'NO_SHIPPING',
                'return_url'  => $return_url,
                'cancel_url'  => $cancel_url,
            ],
        ]);

        [$status, $body] = pp_http(
            'POST',
            pp_api_base($env) . '/v2/checkout/orders',
            [
                'Authorization: Bearer ' . pp_token($pdo),
                'Content-Type: application/json',
            ],
            $payload
        );

        if (($status !== 200 && $status !== 201) || empty($body['id'])) {
            throw new RuntimeException('PayPal create-order failed (HTTP ' . $status . '): ' . json_encode($body));
        }

        $approve = '';
        foreach ($body['links'] ?? [] as $lnk) {
            if (($lnk['rel'] ?? '') === 'approve') { $approve = $lnk['href']; break; }
        }
        if ($approve === '') {
            throw new RuntimeException('PayPal create-order returned no approval link.');
        }
        return ['id' => $body['id'], 'approve' => $approve];
    }

    /**
     * Capture a previously created order. Returns the decoded capture body.
     * Caller must verify status/amount/custom_id against expectations.
     */
    function pp_capture_order(PDO $pdo, string $orderId): array
    {
        $s   = pp_settings($pdo);
        $env = $s['environment'] ?? 'sandbox';

        [$status, $body] = pp_http(
            'POST',
            pp_api_base($env) . '/v2/checkout/orders/' . rawurlencode($orderId) . '/capture',
            [
                'Authorization: Bearer ' . pp_token($pdo),
                'Content-Type: application/json',
                // Idempotency: a retried capture of the same order won't double-charge.
                'PayPal-Request-Id: capture-' . $orderId,
            ],
            '{}'
        );

        return ['http' => $status, 'body' => $body];
    }

    /**
     * Pull the essential facts out of a capture response:
     * [completed(bool), amount(string), currency(string), custom_id(string), capture_id(string)].
     */
    function pp_capture_facts(array $capture): array
    {
        $body = $capture['body'] ?? [];
        $pu   = $body['purchase_units'][0] ?? [];
        $cap  = $pu['payments']['captures'][0] ?? [];
        return [
            'completed'  => (($body['status'] ?? '') === 'COMPLETED') && (($cap['status'] ?? '') === 'COMPLETED'),
            'amount'     => (string)($cap['amount']['value'] ?? ''),
            'currency'   => (string)($cap['amount']['currency_code'] ?? ''),
            'custom_id'  => (string)($cap['custom_id'] ?? $pu['custom_id'] ?? ''),
            'capture_id' => (string)($cap['id'] ?? ''),
        ];
    }
}
