<?php

declare(strict_types=1);

namespace App\Http\Controllers\Api;

use App\Http\Controllers\Controller;
use App\Models\PaymentGateway;
use App\Models\SaasTransaction;
use App\Services\Billing\BillingTopupService;
use App\Services\Billing\Gateway\PaymentGatewayDriver;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Symfony\Component\HttpFoundation\IpUtils;

/**
 * Приём webhook от платёжного шлюза (ЮKassa). Публичный роут (без auth/tenant),
 * URL под маской api/webhook/* → CSRF-exempt (bootstrap/app.php).
 *
 * Подлинность: НЕ доверяем телу webhook — по object.id делаем server-to-server
 * сверку через драйвер (GET /payments/{id}) и верим статусу из ответа шлюза.
 *
 * RLS: webhook вне tenant-сессии. Поиск платежа cross-tenant — через
 * BYPASSRLS-соединение pgsql_supplier (как джобы, Plan 3/4). Зачисление —
 * под app.current_tenant_id (SET LOCAL внутри транзакции, как BalancePreflightSweepJob).
 *
 * Идемпотентность: атомарный claim pending→success (UPDATE ... WHERE status='pending').
 * Повторный webhook → claimed=0 → no-op, 200 OK без двойного зачисления.
 *
 * Зачисление денег делегируется BillingTopupService (lockForUpdate + append-only ledger).
 */
class PaymentWebhookController extends Controller
{
    public function __construct(
        private readonly PaymentGatewayDriver $driver,
        private readonly BillingTopupService $topupService,
    ) {}

    public function receive(Request $request): JsonResponse
    {
        // Defense-in-depth: IP-allowlist ЮKassa. Fail-open при пустом списке —
        // не ломаем легитимный поток; на проде заполнить YOOKASSA_WEBHOOK_IPS
        // опубликованными ЮKassa подсетями, чтобы аноним не дёргал endpoint.
        $allowlist = array_values(array_filter((array) config('services.yookassa.webhook_ip_allowlist', [])));
        if ($allowlist !== [] && ! IpUtils::checkIp((string) $request->ip(), $allowlist)) {
            return response()->json(['status' => 'ignored'], 200);
        }

        $paymentId = (string) $request->input('object.id', '');
        if ($paymentId === '') {
            return response()->json(['status' => 'ignored'], 200);
        }

        // Cross-tenant поиск платежа под BYPASSRLS-ролью (tenant ещё неизвестен).
        $tx = DB::connection('pgsql_supplier')->table('saas_transactions')
            ->where('gateway_payment_id', $paymentId)
            ->first();
        if ($tx === null) {
            return response()->json(['status' => 'unknown'], 200);
        }

        $gateway = $this->gatewayFor($tx);

        // Server-to-server сверка — источник правды о статусе.
        $verify = $this->driver->verifyPayment($gateway, $paymentId);
        if (! $verify->isSucceeded()) {
            return response()->json(['status' => 'not_paid'], 200);
        }

        // Confused-deputy: сверенный платёж должен быть РОВНО тем, что в webhook.
        if ($verify->gatewayPaymentId !== $paymentId) {
            return response()->json(['status' => 'ignored'], 200);
        }

        // Защита от чужой валюты с тем же числом — зачисляем только рубли.
        if ($verify->currency !== 'RUB') {
            return response()->json(['status' => 'currency_mismatch'], 200);
        }

        // Защита: оплаченная сумма должна совпасть с запрошенной (scale 2).
        if (bccomp((string) $verify->amountRub, (string) $tx->amount_rub, 2) !== 0) {
            return response()->json(['status' => 'amount_mismatch'], 200);
        }

        DB::transaction(function () use ($tx, $verify) {
            // RLS-контекст для этой транзакции (PgBouncer-safe SET LOCAL).
            DB::statement('SET LOCAL app.current_tenant_id = '.(int) $tx->tenant_id);

            // Атомарно занимаем pending→success; 0 строк = уже зачислено (дубль/гонка).
            $claimed = SaasTransaction::where('id', $tx->id)
                ->where('status', SaasTransaction::STATUS_PENDING)
                ->update(['status' => SaasTransaction::STATUS_SUCCESS, 'completed_at' => now()]);

            if ($claimed === 0) {
                return; // идемпотентный no-op
            }

            $balanceTx = $this->topupService->topup(
                (int) $tx->tenant_id, (string) $tx->amount_rub, null
            );

            SaasTransaction::where('id', $tx->id)->update([
                'balance_rub_after' => $balanceTx->balance_rub_after,
                'payment_method' => $verify->paymentMethod,
                'balance_transaction_id' => $balanceTx->id, // provenance: оплата → строка журнала
            ]);
        });

        return response()->json(['status' => 'ok'], 200);
    }

    private function gatewayFor(object $tx): PaymentGateway
    {
        return $tx->gateway_id !== null
            ? PaymentGateway::findOrFail($tx->gateway_id)
            : PaymentGateway::where('code', $tx->gateway_code)->firstOrFail();
    }
}
