<?php

declare(strict_types=1);

namespace App\Jobs;

use App\Models\Credit;
use App\Services\CreditStatusSyncService;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\Middleware\WithoutOverlapping;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Log;

/**
 * Sincroniza el estado de un crédito de forma asíncrona tras un pago.
 *
 * Diseño de payload mínimo:
 *   Solo se serializa el credit_id (int). No se pasa el modelo Credit
 *   completo para evitar snapshots obsoletos: el worker siempre hace
 *   un fresh read desde DB con installments cargados, viendo el estado
 *   post-pago más reciente.
 *
 * Thread-safety:
 *   WithoutOverlapping serializa los jobs del mismo credit_id, evitando
 *   race conditions cuando múltiples pagos se procesan rápidamente para
 *   el mismo crédito. Si ya hay un job en curso para el crédito, el
 *   nuevo se re-encola automáticamente tras releaseAfter segundos.
 *
 * Reintentos:
 *   3 intentos con backoff progresivo (1 min → 5 min → 15 min) para
 *   absorber picos de carga en DB sin amplificar la presión.
 */
class SyncCreditStatusJob implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    /**
     * Número máximo de reintentos antes de marcar el job como fallido.
     */
    public int $tries = 3;

    /**
     * Backoff progresivo entre reintentos en segundos.
     * 60s → 300s → 900s reduce el impacto en DB bajo alta carga.
     */
    public array $backoff = [60, 300, 900];

    /**
     * Timeout del job en segundos.
     */
    public int $timeout = 30;

    /**
     * @param  int  $creditId  ID del crédito a sincronizar.
     *                         Único campo serializado: no se pasa el modelo completo.
     */
    public function __construct(
        public readonly int $creditId,
    ) {}

    /**
     * Hidrata el crédito con sus installments (fresh read desde DB)
     * y ejecuta la sincronización de estado.
     *
     * El fresh read garantiza que el worker vea el estado de la DB
     * después del pago, no un snapshot serializado anterior.
     */
    public function handle(CreditStatusSyncService $syncService): void
    {
        // Carga installments (para resolver estado) y children (para isParent() relation-aware,
        // evitando una query extra por crédito en canAutoUpdateStatus()).
        $credit = Credit::with(['installments', 'children'])->find($this->creditId);

        if (! $credit) {
            // El crédito fue eliminado entre el dispatch y el procesamiento
            Log::info('SyncCreditStatusJob: crédito no encontrado, omitiendo.', [
                'credit_id' => $this->creditId,
            ]);

            return;
        }

        $syncService->forceSync($credit);
    }

    /**
     * Middleware: serializa el procesamiento por credit_id.
     *
     * Si ya existe un job en ejecución para este crédito:
     * - releaseAfter(90): re-encola el job actual con 90s de delay para dar
     *   margen suficiente a operaciones lentas (reestructuraciones, sync pesados).
     * - expireAfter(180): el lock expira en 180s como safety net ante crashes
     *   del worker. Antes era 60s, lo que podía dejar el lock colgado si el
     *   proceso tardaba más de un minuto (e.g. bajo carga de DB).
     */
    public function middleware(): array
    {
        return [
            (new WithoutOverlapping((string) $this->creditId))
                ->releaseAfter(90)
                ->expireAfter(180),
        ];
    }

    /**
     * Callback ejecutado cuando el job agota todos sus reintentos.
     */
    public function failed(\Throwable $exception): void
    {
        Log::error('SyncCreditStatusJob: agotó todos los reintentos.', [
            'credit_id' => $this->creditId,
            'error' => $exception->getMessage(),
            'file' => $exception->getFile(),
            'line' => $exception->getLine(),
        ]);
    }
}
