<?php

declare(strict_types=1);

namespace App\Jobs;

use App\Models\Deal;
use App\Models\Tenant;
use App\Services\NotificationService;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Database\Eloquent\Collection as EloquentCollection;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;

/**
 * G2-A: раз в 30 минут (routes/console.php) рассылает дайджест новых сделок.
 * Окно — последние 30 минут по received_at.
 *
 * Идемпотентность по СДЕЛКЕ (N-4): окно даёт защиту только при ровно-30-мин
 * прогонах; ручной/повторный прогон (R3b велит дёргать вручную) перекрывает окно.
 * Поэтому каждая сделка, попавшая в дайджест, помечается в Redis (TTL 1 сутки) —
 * повторно в дайджест не включается. Пометка ставится ПОСЛЕ успешной отправки
 * (mark-after-send): падение джоба до неё оставит сделки непомеченными → очередь
 * повторит (at-least-once вместо тихой потери). Один воркер → гонки нет.
 */
final class SendNewLeadsDigestJob implements ShouldQueue
{
    use Dispatchable;
    use InteractsWithQueue;
    use Queueable;

    public function handle(NotificationService $notifier): void
    {
        Tenant::query()->whereNull('deleted_at')->chunkById(200, function (EloquentCollection $tenants) use ($notifier): void {
            foreach ($tenants as $tenant) {
                /** @var Tenant $tenant */
                $this->digestForTenant($tenant, $notifier);
            }
        });
    }

    private function digestForTenant(Tenant $tenant, NotificationService $notifier): void
    {
        DB::transaction(function () use ($tenant, $notifier): void {
            DB::statement('SET LOCAL app.current_tenant_id = '.(int) $tenant->id);

            $deals = Deal::query()
                ->where('tenant_id', $tenant->id)
                ->where('received_at', '>', now()->subMinutes(30))
                ->where('is_test', false)
                ->whereNull('deleted_at')
                ->orderBy('received_at')
                ->get();

            // N-4: исключаем сделки, уже попавшие в прошлый дайджест (без side-effect).
            $fresh = $deals->reject(
                fn (Deal $deal): bool => Cache::has('digest_sent:'.$deal->id)
            );

            if ($fresh->isEmpty()) {
                return;
            }

            $notifier->notifyNewLeadsDigest($tenant, $fresh);

            // N-4: помечаем ТОЛЬКО ПОСЛЕ успешного возврата notify. Падение джоба
            // до этой точки оставит сделки непомеченными → очередь повторит прогон
            // (at-least-once вместо тихой потери дайджеста). Один воркер (см.
            // prod-logic-map §18.4) → гонки между прогонами нет. TTL 1 сутки.
            foreach ($fresh as $deal) {
                Cache::put('digest_sent:'.$deal->id, 1, now()->addDay());
            }
        });
    }
}
