<?php
// Lucky Cobros · servidor mínimo que recibe los avisos (webhooks) y verifica la firma.
//
// Uso (PHP 7.4 o superior, sin dependencias):
//   WEBHOOK_SECRET="el-mismo-secreto-que-configuraste" php -S localhost:3000 verificar-firma.php
// Escucha en http://localhost:3000/webhooks/cobros
// (En Windows PowerShell: $env:WEBHOOK_SECRET="..."; php -S localhost:3000 verificar-firma.php)
//
// Reglas que sigue este ejemplo:
//   1. La firma se calcula sobre el cuerpo CRUDO (php://input). No uses json_encode para rearmarlo.
//   2. La comparación es en tiempo constante (hash_equals).
//   3. Responde 200 enseguida y procesa después.
//   4. Deduplica: el mismo aviso no se procesa dos veces.
declare(strict_types=1);

const RUTA = '/webhooks/cobros';

$secreto = getenv('WEBHOOK_SECRET') ?: '';
if ($secreto === '') {
    http_response_code(500);
    error_log('Falta WEBHOOK_SECRET: usá el mismo secreto que configuraste con PUT /external/webhook.');
    exit;
}

/** true si el encabezado X-Webhook-Signature corresponde a este cuerpo y este secreto. */
function firmaValida(string $secreto, string $cuerpoCrudo, string $encabezado): bool
{
    $recibida = strtolower((string) preg_replace('/^sha256=/i', '', trim($encabezado)));
    if (!preg_match('/^[0-9a-f]{64}$/', $recibida)) {
        return false;
    }
    $esperada = hash_hmac('sha256', $cuerpoCrudo, $secreto);
    return hash_equals($esperada, $recibida);
}

/** Clave para no procesar dos veces el mismo aviso (null = no hay código para deduplicar). */
function claveDeDuplicado(array $aviso): ?string
{
    $evento = (string) ($aviso['event'] ?? '');
    $d = $aviso['data'] ?? [];
    // Un mismo cobro puede recibir transaction.in_review y después confirmed o refunded: la clave lleva el evento.
    if (strpos($evento, 'transaction.') === 0) {
        return isset($d['id']) ? $evento . ':' . $d['id'] : null;
    }
    if (strpos($evento, 'withdrawal.') === 0) {
        return isset($d['coelsa_id']) ? $evento . ':' . $d['coelsa_id'] : null;
    }
    return null;
}

/** Devuelve true la primera vez que ve la clave. En producción, usá tu base con un índice único. */
function primeraVez(string $clave): bool
{
    // Fuera de la carpeta pública del servidor web: si quedara adentro, se podría descargar.
    $archivo = sys_get_temp_dir() . '/lucky-cobros-avisos-procesados.txt';
    $fp = fopen($archivo, 'c+');
    flock($fp, LOCK_EX);
    $vistos = array_filter(explode("\n", (string) stream_get_contents($fp)));
    $nueva = !in_array($clave, $vistos, true);
    if ($nueva) {
        fseek($fp, 0, SEEK_END);
        fwrite($fp, $clave . "\n");
    }
    flock($fp, LOCK_UN);
    fclose($fp);
    return $nueva;
}

function procesar(array $aviso): void
{
    $d = $aviso['data'] ?? [];
    switch ($aviso['event']) {
        case 'transaction.confirmed':
            // Ya está en tu saldo: acreditá $d['monto'] al jugador con CUIT $d['contraparte_cuil_cuit'].
            error_log(sprintf('[cobro] %s · acreditar $ %s de %s (%s) · código %s', $aviso['event'], $d['monto'],
                $d['contraparte_nombre'] ?? '', $d['contraparte_cuil_cuit'] ?? '', $d['codigo_coelsa'] ?? ''));
            break;
        case 'transaction.in_review':
        case 'transaction.refunded':
            // No acredites. in_review: queda retenido hasta que tu equipo lo resuelva en el panel (después llega
            // confirmed o refunded con el mismo data.id). refunded: se le devolvió a quien transfirió.
            error_log(sprintf('[cobro] %s · no se acredita · $ %s de %s · motivo %s', $aviso['event'], $d['monto'],
                $d['contraparte_nombre'] ?? '', $d['motivo'] ?? '-'));
            break;
        case 'withdrawal.completed':
        case 'withdrawal.pending':
        case 'withdrawal.failed':
            // Buscá tu retiro abierto por cbu + monto (o por coelsa_id) y actualizá su estado.
            error_log(sprintf('[retiro] %s · status %s · $ %s a %s', $aviso['event'], $d['status'], $d['monto'], $d['cbu']));
            break;
        default:
            error_log('[aviso] evento nuevo, lo ignoramos: ' . $aviso['event']);
    }
}

$ruta = parse_url($_SERVER['REQUEST_URI'] ?? '/', PHP_URL_PATH);
if (($_SERVER['REQUEST_METHOD'] ?? '') !== 'POST' || $ruta !== RUTA) {
    http_response_code(404);
    exit;
}

$cuerpoCrudo = (string) file_get_contents('php://input');
$firma = $_SERVER['HTTP_X_WEBHOOK_SIGNATURE'] ?? '';

header('Content-Type: application/json');

if (!firmaValida($secreto, $cuerpoCrudo, $firma)) {
    http_response_code(401);
    echo '{"ok":false,"motivo":"firma inválida"}';
    exit;
}

$aviso = json_decode($cuerpoCrudo, true);
if (!is_array($aviso) || !isset($aviso['event'])) {
    http_response_code(400);
    echo '{"ok":false,"motivo":"JSON inválido"}';
    exit;
}

// 1) Respondé 2xx rápido.
http_response_code(200);
echo '{"ok":true}';
if (function_exists('fastcgi_finish_request')) {
    fastcgi_finish_request(); // con PHP-FPM, corta la respuesta acá y sigue procesando
} else {
    flush();
}

// 2) Procesá después, sin repetir.
$clave = claveDeDuplicado($aviso);
if ($clave !== null && !primeraVez($clave)) {
    error_log('[aviso] repetido, no se procesa de nuevo: ' . $clave);
    exit;
}
procesar($aviso);
