<?php

declare(strict_types=1);

namespace App\Jobs\Billing;

use App\Mail\BalanceFrozenFinalMail;
use App\Mail\BalanceFrozenReminderMail;
use App\Models\PricingTier;
use App\Models\Tenant;
use App\Repositories\PricingTierRepository;
use App\Services\Billing\BalancePreflightService;
use App\Services\Billing\PreflightResult;
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;

/**
 * Повторные письма заморозки баланса:
 *   • reminder в окне 24-48ч после freeze;
 *   • final в окне 72-96ч после freeze.
 *
 * Throttle через balance_freeze_log markers (event_type 'reminder_sent' / 'final_sent') —
 * один marker-row на (tenant, тип) в течение окна 5 дней. Запускается daily @ 18:30 MSK
 * (routes/console.php). См. спек §3.7.
 *
 * Re-evaluate PreflightResult: показываем клиенту АКТУАЛЬНЫЙ дефицит (он мог частично
 * пополниться — reminder отразит обновлённую цифру).
 */
final class BalanceFrozenReminderJob implements ShouldQueue
{
    use Dispatchable;
    use InteractsWithQueue;
    use Queueable;

    private const REMINDER_MIN_HOURS = 24;

    private const REMINDER_MAX_HOURS = 48;

    private const FINAL_MIN_HOURS = 72;

    private const FINAL_MAX_HOURS = 96;

    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 policy tenants_self_isolation отдаёт 0 строк (тот же
        // баг, что у BalancePreflightSweepJob). Берём id через pgsql_supplier (BYPASSRLS).
        $tenantIds = DB::connection('pgsql_supplier')->table('tenants')
            ->whereNotNull('frozen_by_balance_at')
            ->whereNull('deleted_at')
            ->orderBy('id')
            ->pluck('id');

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

    /**
     * @param  Collection<int, PricingTier>  $tiers
     */
    private function processTenant(int $tenantId, BalancePreflightService $service, Collection $tiers): void
    {
        // SET LOCAL внутри транзакции восстанавливает tenant-контекст: и Tenant::find,
        // и requiredLeadsForTomorrow() (читает projects) RLS-зависимы. mark()/alreadySent()
        // идут через pgsql_supplier (BYPASSRLS) — им контекст не нужен.
        DB::transaction(function () use ($tenantId, $service, $tiers): void {
            DB::statement('SET LOCAL app.current_tenant_id = '.$tenantId);

            $tenant = Tenant::find($tenantId);
            if ($tenant === null || $tenant->frozen_by_balance_at === null) {
                return; // разморожен/удалён между pluck и обработкой.
            }

            // diffInHours округляет — у заморозки на 25h это 25, на 73h это 73 (OK).
            $hours = (int) abs(now()->diffInHours($tenant->frozen_by_balance_at, false));

            $window = $this->matchWindow($hours);
            if ($window === null) {
                return; // вне окон reminder/final
            }

            $marker = $window === 'reminder' ? 'reminder_sent' : 'final_sent';
            if ($this->alreadySent($tenant->id, $marker)) {
                return;
            }

            // Re-evaluate для актуального дефицита в тексте письма.
            $result = $service->evaluate(
                balanceRub: (string) $tenant->balance_rub,
                deliveredInMonth: (int) $tenant->delivered_in_month,
                requiredLeads: $tenant->requiredLeadsForTomorrow(),
                tiers: $tiers,
            );

            $mail = $window === 'reminder'
                ? new BalanceFrozenReminderMail($tenant, $result)
                : new BalanceFrozenFinalMail($tenant, $result);

            Mail::queue($mail);
            $this->mark($tenant, $marker, $result);
        });
    }

    private function matchWindow(int $hours): ?string
    {
        if ($hours >= self::REMINDER_MIN_HOURS && $hours < self::REMINDER_MAX_HOURS) {
            return 'reminder';
        }
        if ($hours >= self::FINAL_MIN_HOURS && $hours < self::FINAL_MAX_HOURS) {
            return 'final';
        }

        return null;
    }

    private function alreadySent(int $tenantId, string $marker): bool
    {
        return DB::connection('pgsql_supplier')->table('balance_freeze_log')
            ->where('tenant_id', $tenantId)
            ->where('event_type', $marker)
            ->where('created_at', '>=', now()->subDays(5))
            ->exists();
    }

    private function mark(Tenant $tenant, string $marker, PreflightResult $result): void
    {
        DB::connection('pgsql_supplier')->table('balance_freeze_log')->insert([
            'tenant_id' => $tenant->id,
            'event_type' => $marker,
            'triggered_by' => 'reminder_cron',
            '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(),
        ]);
    }
}
