<?php

declare(strict_types=1);

namespace App\Jobs\Billing;

use App\Jobs\SyncSupplierProjectJob;
use App\Mail\BalanceFrozenMail;
use App\Models\PricingTier;
use App\Models\Tenant;
use App\Repositories\PricingTierRepository;
use App\Services\Billing\BalancePreflightService;
use App\Services\Billing\PreflightResult;
use App\Services\Billing\ProjectBlockReleaseService;
use App\Services\Supplier\SupplierExportMode;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Mail;

/**
 * Ежедневный преfflight всех тенантов перед формированием заказа поставщику.
 * Запускается cron @18:00 MSK (routes/console.php). См. спек §3.5, §5.2.
 *
 * NB: бегает без tenant-RLS (системный контекст); запросы к projects/tenants
 * явные по tenant_id (урок Спека B). Переход active→frozen / frozen→active
 * шлёт письмо; стабильное состояние не трогается (идемпотентность).
 */
final class BalancePreflightSweepJob implements ShouldQueue
{
    use Dispatchable;
    use InteractsWithQueue;
    use Queueable;

    public function handle(): void
    {
        $service = new BalancePreflightService;
        // Косяк 01: действующая версия тарифа по дате (как списание/витрина), а не «по-простому».
        $tiers = app(PricingTierRepository::class)->activeAt(now('Europe/Moscow'));

        // Переезд на Managed PG (26.06.2026): очередь ходит в БД под ролью crm_app_user
        // (RLS). Перечень тенантов брать через ДЕФОЛТНОЕ соединение нельзя — без
        // app.current_tenant_id RLS-policy tenants_self_isolation отдаёт 0 строк, и
        // sweep молча превращался в no-op (ни заморозок, ни снятия блоков). Берём id
        // через pgsql_supplier (BYPASSRLS — системный контекст), как джоба уже делает
        // для balance_freeze_log. Дальше per-tenant SET LOCAL восстанавливает контекст.
        $tenantIds = DB::connection('pgsql_supplier')->table('tenants')
            ->whereNull('deleted_at')
            ->orderBy('id')
            ->pluck('id');

        foreach ($tenantIds as $tenantId) {
            $this->evaluateTenant((int) $tenantId, $service, $tiers);
        }
    }

    /**
     * @param  Collection<int, PricingTier>  $tiers
     */
    private function evaluateTenant(int $tenantId, BalancePreflightService $service, Collection $tiers): void
    {
        // Spec C deploy hotfix (25.05.2026): CLI-команды и фоновые джобы не проходят
        // через SetTenantContext middleware → app.current_tenant_id не выставлен →
        // RLS-policy на projects падает с "unrecognized configuration parameter".
        // Зеркалим mechanic SetTenantContext: SET LOCAL внутри транзакции (PgBouncer-safe).
        DB::transaction(function () use ($tenantId, $service, $tiers): void {
            DB::statement('SET LOCAL app.current_tenant_id = '.$tenantId);

            // Модель грузим ВНУТРИ контекста — под RLS-ролью без SET LOCAL Tenant::find
            // вернёт null (id-isolation policy). После SET LOCAL запись своей компании видна.
            $tenant = Tenant::find($tenantId);
            if ($tenant === null) {
                return; // удалён между pluck и обработкой — пропускаем.
            }

            $required = $tenant->requiredLeadsForTomorrow();
            $result = $service->evaluate(
                balanceRub: (string) $tenant->balance_rub,
                deliveredInMonth: (int) $tenant->delivered_in_month,
                requiredLeads: $required,
                tiers: $tiers,
            );

            $isFrozen = $tenant->frozen_by_balance_at !== null;

            if (! $result->passes) {
                // Переход active → frozen (разморозку/снятие блоков здесь НЕ делаем —
                // заморозка главнее, см. иерархию J спеки balance-lock-unify-FJ).
                if (! $isFrozen) {
                    $freezeAt = now();
                    $tenant->frozen_by_balance_at = $freezeAt;
                    $tenant->save();

                    // Stage 3 R-13 (spec §4.3.2): помечаем все непаузнутые проекты
                    // тенанта моментом заморозки. Это даёт SupplierSnapshotGuard
                    // зацепку (paused_at свежее grace-периода) — клиент не сможет
                    // удалить/сменить источник пока хвост слепка ещё может прилететь.
                    DB::connection('pgsql_supplier')->table('projects')
                        ->where('tenant_id', $tenant->id)
                        ->whereNull('paused_at')
                        ->update(['paused_at' => $freezeAt]);

                    $this->logEvent($tenant, 'frozen', 'cutoff_18msk', $result);
                    Mail::queue(new BalanceFrozenMail($tenant, $result));
                    $this->dispatchSupplierSyncIfOnline($tenant);
                }

                return; // заморожен и не хватает — стабильное состояние, блоки не трогаем.
            }

            // passes → единый путь разблокировки (D6): разморозить клиента (если был, J)
            // + снять блоки всех проектов (F). Идемпотентно: нет замков → no-op.
            (new ProjectBlockReleaseService)->releaseForTenant($tenant->id);
        });
    }

    /**
     * Spec C extension (26.05.2026): при переходе freeze ↔ unfreeze в режиме 'online'
     * диспатчим точечный sync с поставщиком per-project (group-recalc внутри handleOnline
     * сам учтёт шеринг через signal_identifier). В режиме 'batch' изменения уезжают
     * cut-off cron'ом @18:00 MSK через SyncSupplierProjectsJob (множественный).
     * Привязка к админ-переключателю SupplierExportMode (system_settings.supplier_export_mode).
     *
     * Вызывается ВНУТРИ DB::transaction обёртки evaluateTenant — app.current_tenant_id выставлен,
     * RLS-фильтрация projects работает.
     */
    private function dispatchSupplierSyncIfOnline(Tenant $tenant): void
    {
        if (! SupplierExportMode::isOnline()) {
            return;
        }

        $projectIds = $tenant->projects()
            ->where('is_active', true)
            ->whereNull('preflight_blocked_at')
            ->pluck('id');

        foreach ($projectIds as $id) {
            SyncSupplierProjectJob::dispatch((int) $id);
        }
    }

    private function logEvent(Tenant $tenant, string $event, string $trigger, PreflightResult $result): void
    {
        DB::connection('pgsql_supplier')->table('balance_freeze_log')->insert([
            'tenant_id' => $tenant->id,
            'event_type' => $event,
            'triggered_by' => $trigger,
            'balance_rub_at_event' => $tenant->balance_rub,
            'required_leads' => $result->requiredLeads,
            'capacity_leads' => $result->capacityLeads,
            'total_daily_limit' => $result->requiredLeads,
            'details' => json_encode(['deficit_leads' => $result->deficitLeads]),
            'created_at' => now(),
        ]);
    }
}
