<?php

declare(strict_types=1);

namespace App\Http\Controllers\Api;

use App\Http\Controllers\Controller;
use App\Jobs\RouteSupplierLeadJob;
use App\Models\SupplierLead;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Symfony\Component\HttpFoundation\IpUtils;

/**
 * Платформенный endpoint для входящего stream'а лидов от crm.bp-gr.ru.
 *
 * URL: POST /api/webhook/supplier/{secret}
 *
 * Защита (defense-in-depth, spec §5.1):
 *   1. {secret} в URL — platform-wide токен (≥32 chars), хранится в
 *      system_settings.supplier_webhook_secret. hash_equals для сравнения.
 *   2. IP allowlist (system_settings.supplier_ip_allowlist) — JSON array IP/CIDR.
 *      Пустой массив = пропускать всех (DEV mode); на prod заполнить.
 *
 * Несовпадение secret OR IP → 404 (не палим существование endpoint'а).
 *
 * Идемпотентность: UNIQUE INDEX на supplier_leads.vid. При дубле возвращаем
 * 200 OK без re-dispatch (поставщик может ретранслировать одни и те же лиды).
 *
 * Backward-compat: legacy /api/webhook/{token} (per-tenant) живёт параллельно
 * на WebhookReceiveController — не пересекается.
 *
 * Plan 2.6 fix #ii (10.05.2026): пустой `supplier_ip_allowlist = '[]'` на
 * production env теперь fail-closed (`verifyIpAllowlist` возвращает false если
 * env=production AND allowlist пустой). На dev/testing — fail-open для localhost
 * development. Admin ОБЯЗАН заполнить allowlist реальными IP/CIDR поставщика
 * перед production deploy. Дополнительно: hash_equals на уровне `verifySecret`
 * timing-safe только при равной длине; rate-limit per-IP добавится в Plan 3+.
 */
class SupplierWebhookController extends Controller
{
    public function receive(Request $request, string $secret): JsonResponse
    {
        if (! $this->verifySecret($secret)) {
            return response()->json(['message' => 'Not found.'], 404);
        }

        if (! $this->verifyIpAllowlist($request->ip())) {
            return response()->json(['message' => 'Not found.'], 404);
        }

        // Plan 2.6 fix #iii: timestamp partition guard. Партиции deals месячные
        // (deals_2026_MM); time за пределами текущего месяца → INSERT CRASH
        // "no partition of relation deals found for row" в RouteSupplierLeadJob.
        // Окно ±24h защищает от wildly out-of-range значений (старый/будущий
        // дроп от поставщика); покрывает retry-задержки + clock-drift серверов.
        $minTime = now()->subDay()->getTimestamp();
        $maxTime = now()->addDay()->getTimestamp();

        $validated = $request->validate([
            'vid' => 'required|integer|min:1',
            'project' => ['required', 'string', 'max:255', 'regex:/^B[123]_.+$/'],
            'phone' => ['required', 'string', 'regex:/^7\d{10}$/'],
            'time' => ['required', 'integer', "min:{$minTime}", "max:{$maxTime}"],
            'tag' => 'nullable|string|max:255',
            'phones' => 'nullable|array',
            'phones.*' => 'string|regex:/^7\d{10}$/',
        ]);

        $existing = SupplierLead::query()->where('vid', $validated['vid'])->first();
        if ($existing !== null) {
            return response()->json([
                'status' => 'already_processed',
                'supplier_lead_id' => $existing->id,
            ], 200);
        }

        $platform = $this->parsePlatform($validated['project']);

        $lead = SupplierLead::create([
            'platform' => $platform,
            'raw_payload' => $validated,
            'vid' => $validated['vid'],
            'phone' => $validated['phone'],
            'received_at' => now(),
            'source' => 'webhook',
        ]);

        RouteSupplierLeadJob::dispatch($lead->id);

        return response()->json([
            'status' => 'accepted',
            'supplier_lead_id' => $lead->id,
        ], 202);
    }

    private function verifySecret(string $providedSecret): bool
    {
        $row = DB::table('system_settings')->where('key', 'supplier_webhook_secret')->first();
        if ($row === null) {
            return false;
        }
        $expected = (string) $row->value;
        if ($expected === '__SET_ON_DEPLOY__' || strlen($expected) < 32) {
            return false;
        }

        return hash_equals($expected, $providedSecret);
    }

    private function verifyIpAllowlist(?string $clientIp): bool
    {
        if ($clientIp === null) {
            return false;
        }
        $row = DB::table('system_settings')->where('key', 'supplier_ip_allowlist')->first();
        if ($row === null) {
            return true;
        }
        $list = json_decode((string) $row->value, true) ?: [];
        if ($list === []) {
            // Plan 2.6 fix #ii: production env — пустой allowlist fail-closed (защита
            // от забытого override schema seed); dev/testing — fail-open для localhost
            // development. CV.11 audit WARN #5: inline-warning lines 34-39 признавал
            // проблему, теперь enforced на уровне env.
            return ! app()->environment('production');
        }

        return IpUtils::checkIp($clientIp, $list);
    }

    private function parsePlatform(string $project): string
    {
        preg_match('/^(B[123])_/', $project, $m);

        return $m[1] ?? 'B1';
    }
}
