<?php

declare(strict_types=1);

namespace App\Http\Controllers;

use App\Services\Payments\PaymentGateway;
use App\Services\Payments\PaymentService;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Support\Facades\Log;
use Throwable;

/**
 * Where Revolut tells us a payment landed.
 *
 * Four rules, in this order, and the order is the point:
 *
 *  1. Verify the signature before reading anything. Until that passes, the
 *     body is a stranger's opinion about our bookings.
 *  2. Store the event, keyed on its id. Revolut retries on any non-2xx, so
 *     duplicate deliveries are routine rather than exceptional.
 *  3. Do the work inline, not on the queue. The queue drains from cron every
 *     five minutes on the target host, and a customer staring at a "paid"
 *     screen for five minutes will phone the studio. Confirming a booking is
 *     three queries.
 *  4. Return 200 for anything we have safely stored, even if it meant
 *     nothing to us. A 500 on an event we simply do not care about would
 *     have Revolut retrying it for hours.
 */
class RevolutWebhookController extends Controller
{
    public function __invoke(
        Request $request,
        PaymentGateway $gateway,
        PaymentService $payments,
    ): Response {
        // getContent() is the raw body exactly as sent. Re-encoding
        // $request->all() here would change whitespace and key order, and
        // every signature would fail.
        $rawBody = $request->getContent();

        if (! $gateway->verifyCallback($rawBody, $this->headers($request))) {
            Log::warning('Rejected a Revolut webhook with a bad signature.', [
                'ip' => $request->ip(),
            ]);

            // 401, not 400: this is an authentication failure, and Revolut
            // should not keep retrying a request we will never accept.
            return response('Invalid signature', 401);
        }

        $payload = json_decode($rawBody, true);

        if (! is_array($payload)) {
            return response('Malformed payload', 400);
        }

        $event = $gateway->parseEvent($payload);
        $record = $payments->recordEvent($event);

        if ($record === null) {
            // Seen before. Acknowledge and do nothing — this is the retry
            // path, and it must be cheap and completely side-effect free.
            return response('Duplicate', 200);
        }

        try {
            $applied = $payments->applyEvent($event);

            $record->markProcessed($applied ? 'processed' : 'ignored');
        } catch (Throwable $e) {
            $record->markProcessed('failed', $e->getMessage());

            Log::error('Failed to apply a Revolut webhook.', [
                'event_id' => $event->id,
                'type' => $event->type,
                'error' => $e->getMessage(),
            ]);

            // 500 so Revolut retries. The row is left marked "failed", which
            // recordEvent() treats as retryable rather than as a duplicate,
            // so the next delivery genuinely re-runs this instead of being
            // waved through as already handled.
            return response('Processing failed', 500);
        }

        return response('OK', 200);
    }

    /** @return array<string, string> */
    private function headers(Request $request): array
    {
        $headers = [];

        foreach ($request->headers->all() as $key => $values) {
            $headers[$key] = is_array($values) ? (string) ($values[0] ?? '') : (string) $values;
        }

        return $headers;
    }
}
