<?php
/**
 * TechShop Perú - Cliente PayPal REST API (Orders v2)
 *
 * Verifica y captura órdenes de PayPal de forma segura desde el servidor.
 * Nunca se debe confiar en los datos del cliente: toda orden debe confirmarse
 * contra la API de PayPal antes de marcarse como pagada.
 *
 * @package TechShop Perú
 * @version 1.0
 */

if (!defined('SECURE_ACCESS')) {
    die('Acceso directo no permitido');
}

/**
 * Tasa de cambio soles → USD usada para PayPal.
 * Centralizada aquí para que coincida con el frontend (PAYPAL_USD_RATE).
 */
function paypal_usd_rate(): float
{
    $rate = defined('PAYPAL_USD_RATE') ? (float) PAYPAL_USD_RATE : 3.75;
    return $rate > 0 ? $rate : 3.75;
}

/**
 * Convierte un monto en soles al equivalente aproximado en USD.
 * Debe coincidir con el cálculo usado en pages/checkout.php (frontend).
 */
function paypal_usd_amount(float $soles): float
{
    return round($soles / paypal_usd_rate(), 2);
}

/**
 * Compara el monto capturado con el esperado con una pequeña tolerancia
 * para evitar falsos rechazos por redondeo de moneda.
 */
function paypal_amount_matches(float $captured, float $expected): bool
{
    return abs($captured - $expected) < 0.02;
}

/**
 * Cliente mínimo para la API REST de PayPal (Orders v2).
 *
 * Permite inyectar un transporte HTTP (para pruebas unitarias) mediante
 * el parámetro $transport: callable($method, $url, $headers, $body)
 * debe devolver ['status' => int, 'body' => array].
 */
class PayPalClient
{
    private string $clientId;
    private string $clientSecret;
    private string $baseUrl;

    /** @var callable|null */
    private $transport;

    public function __construct(string $clientId, string $clientSecret, string $mode = 'sandbox', ?callable $transport = null)
    {
        $this->clientId = $clientId;
        $this->clientSecret = $clientSecret;
        $this->baseUrl = strtolower($mode) === 'live'
            ? 'https://api-m.paypal.com'
            : 'https://api-m.sandbox.paypal.com';
        $this->transport = $transport;
    }

    /**
     * Ejecuta una petición HTTP (curl por defecto; transport inyectable para pruebas).
     *
     * @return array{status:int, body:array, error?:string}
     */
    private function request(string $method, string $path, array $headers = [], ?string $body = null): array
    {
        if ($this->transport !== null) {
            return call_user_func($this->transport, $method, $this->baseUrl . $path, $headers, $body);
        }

        if (!function_exists('curl_init')) {
            throw new RuntimeException('cURL no está disponible en el servidor.');
        }

        $ch = curl_init($this->baseUrl . $path);
        $options = [
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_TIMEOUT        => 20,
            CURLOPT_HTTPHEADER     => $headers,
            CURLOPT_CUSTOMREQUEST  => $method,
        ];
        if ($body !== null) {
            $options[CURLOPT_POSTFIELDS] = $body;
        }
        curl_setopt_array($ch, $options);

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

        if ($response === false) {
            return ['status' => 0, 'body' => [], 'error' => $error];
        }

        return ['status' => $status, 'body' => json_decode($response, true) ?: []];
    }

    /**
     * Obtiene un access token con credenciales de cliente (client_credentials).
     *
     * @throws RuntimeException Si PayPal no emite token.
     */
    public function getAccessToken(): string
    {
        $auth = base64_encode($this->clientId . ':' . $this->clientSecret);
        $res  = $this->request('POST', '/v1/oauth2/token', [
            'Authorization: Basic ' . $auth,
            'Content-Type: application/x-www-form-urlencoded',
            'Accept: application/json',
        ], 'grant_type=client_credentials');

        if ($res['status'] !== 200 || empty($res['body']['access_token'] ?? '')) {
            $detail = $res['error'] ?? json_encode($res['body'] ?? []);
            error_log('PayPal token error (HTTP ' . $res['status'] . '): ' . $detail);
            throw new RuntimeException('No se pudo autenticar con PayPal.');
        }

        return (string) $res['body']['access_token'];
    }

    /**
     * Consulta el estado de una orden en PayPal.
     *
     * @return array{status:int, body:array}
     */
    public function getOrder(string $orderId): array
    {
        $token = $this->getAccessToken();
        return $this->request('GET', '/v2/checkout/orders/' . rawurlencode($orderId), [
            'Authorization: Bearer ' . $token,
            'Content-Type: application/json',
        ]);
    }

    /**
     * Captura una orden aprobada (server-side capture).
     *
     * @return array{status:int, body:array}
     */
    public function captureOrder(string $orderId): array
    {
        $token = $this->getAccessToken();
        return $this->request('POST', '/v2/checkout/orders/' . rawurlencode($orderId) . '/capture', [
            'Authorization: Bearer ' . $token,
            'Content-Type: application/json',
            'Accept: application/json',
        ], '{}');
    }

    /**
     * Extrae y valida la captura completada de una orden ya capturada.
     *
     * @param array $orderBody Cuerpo de la orden (capture o getOrder)
     * @return array{amount:float, currency:string, capture_id:string}
     * @throws RuntimeException Si la orden o su captura no están completadas.
     */
    public function extractCapture(array $orderBody): array
    {
        if (($orderBody['status'] ?? '') !== 'COMPLETED') {
            throw new RuntimeException('La orden no está completada en PayPal.');
        }

        $capture = $orderBody['purchase_units'][0]['payments']['captures'][0] ?? null;
        if (!$capture || ($capture['status'] ?? '') !== 'COMPLETED') {
            throw new RuntimeException('No se encontró una captura completada.');
        }

        return [
            'amount'     => (float) ($capture['amount']['value'] ?? 0),
            'currency'   => (string) ($capture['amount']['currency_code'] ?? 'USD'),
            'capture_id' => (string) ($capture['id'] ?? ''),
        ];
    }

    /**
     * Valida la respuesta de una captura server-side y extrae el monto.
     *
     * @param array{status:int, body:array} $response Respuesta de captureOrder()
     * @return array{amount:float, currency:string, capture_id:string}
     * @throws RuntimeException Si PayPal no confirma la captura.
     */
    public function verifyCapture(array $response): array
    {
        $status = (int) ($response['status'] ?? 0);

        if ($status < 200 || $status >= 300) {
            throw new RuntimeException('PayPal rechazó la captura (HTTP ' . $status . ').');
        }

        return $this->extractCapture($response['body'] ?? []);
    }

    /**
     * Verifica una orden que ya fue capturada (p. ej. cuando el frontend
     * la capturó del lado del cliente y el servidor ya no puede capturarla).
     *
     * @return array{amount:float, currency:string, capture_id:string}
     * @throws RuntimeException Si la orden no existe o no está completada.
     */
    public function verifyOrder(string $orderId): array
    {
        $response = $this->getOrder($orderId);
        $status   = (int) ($response['status'] ?? 0);

        if ($status < 200 || $status >= 300) {
            throw new RuntimeException('PayPal rechazó la consulta de la orden (HTTP ' . $status . ').');
        }

        return $this->extractCapture($response['body'] ?? []);
    }
}
