<?php

declare(strict_types=1);

namespace App\Console\Commands;

use App\Events\CreditStatusSynced;
use App\Models\Credit;
use App\Models\Installment;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;

/**
 * Data-patch command: corrige créditos con remaining_balance <= 0
 * que por alguna razón de sincronización NO están en estado 'paid'.
 *
 * Causas conocidas que pueden generar este estado inconsistente:
 *  - Queue worker caído → SyncCreditStatusJob nunca se ejecutó
 *  - UpdateCreditStatus legacy (sin guard) marcó un crédito saldado como 'overdue'
 *  - Errores de red durante el registro de pago impidieron el forceSync
 *
 * USO:
 *   php artisan credify:fix-completed-credits           # dry-run (solo muestra)
 *   php artisan credify:fix-completed-credits --apply   # aplica los cambios
 *
 * Seguro para correr en producción: usa chunking de 200 registros para
 * evitar picos de memoria, y envuelve cada crédito en una transacción
 * individual para que un error no deshaga todo el lote.
 */
class FixCompletedCredits extends Command
{
    protected $signature = 'credify:fix-completed-credits
        {--apply : Aplica los cambios en la base de datos (por defecto solo simula)}';

    protected $description = 'Corrige créditos con saldo $0 que no están marcados como "paid". Incluye cuotas.';

    public function handle(): int
    {
        $apply = (bool) $this->option('apply');

        $this->info('==============================================');
        $this->info(' CORRECCIÓN DE CRÉDITOS COMPLETADOS');
        $this->info('==============================================');
        $this->line('Modo: '.($apply ? 'APLICANDO CAMBIOS' : 'SOLO LECTURA (dry-run)'));
        $this->newLine();

        $found = 0;
        $fixed = 0;
        $installmentsFix = 0;
        $errors = 0;
        $previewRows = [];

        // Buscar créditos hoja (sin hijos) con remaining_balance <= 0
        // cuyo status NO sea 'paid'. Excluimos estados inmutables de operaciones
        // (extended, refinanced, etc.) para no tocar créditos padre o reestructurados.
        $mutableStates = [
            Credit::STATUS_ACTIVE,
            Credit::STATUS_DELAYED,
            Credit::STATUS_OVERDUE,
        ];

        // remaining_balance es un accessor calculado (no columna real).
        // Usamos subquery contra installments: SUM(total_amount) - SUM(amount_paid) <= 0
        $balanceSubquery = '(SELECT COALESCE(SUM(i.total_amount),0) - COALESCE(SUM(i.amount_paid),0) '
                         .'FROM installments i WHERE i.credit_id = credits.id)';

        Credit::with(['installments', 'children'])
            ->whereDoesntHave('children')
            ->whereIn('status', $mutableStates)
            ->whereRaw("{$balanceSubquery} <= 0")
            ->orderBy('id')
            ->chunkById(200, function ($credits) use (
                $apply,
                &$found,
                &$fixed,
                &$installmentsFix,
                &$errors,
                &$previewRows
            ) {
                foreach ($credits as $credit) {
                    $found++;

                    $pendingCount = $credit->installments
                        ->where('status', '!=', Installment::STATUS_PAID)
                        ->count();

                    $previewRows[] = [
                        $credit->id,
                        $credit->company_id,
                        $credit->status,
                        '$'.number_format((float) $credit->remaining_balance, 2),
                        $pendingCount.' cuota(s) pendiente(s)',
                    ];

                    if (! $apply) {
                        continue;
                    }

                    DB::beginTransaction();
                    try {
                        // 1. Corregir estado del crédito usando saveQuietly para no
                        //    re-disparar observers ni eventos que puedan generar loops.
                        $oldStatus = $credit->status;
                        $credit->status = Credit::STATUS_PAID;
                        $credit->saveQuietly();

                        // 2. Corregir cuotas no saldadas del crédito
                        $pendingInstallments = $credit->installments
                            ->where('status', '!=', Installment::STATUS_PAID);

                        foreach ($pendingInstallments as $installment) {
                            $installment->status = Installment::STATUS_PAID;
                            $installment->amount_paid = $installment->total_amount;
                            $installment->paid_date = now()->toDateString();
                            $installment->saveQuietly();
                            $installmentsFix++;
                        }

                        // 3. Limpiar la ruta del cobrador. saveQuietly() omite el
                        //    CreditObserver, así que el crédito saldado quedaría en
                        //    collector_credit_order. Despachamos CreditStatusSynced
                        //    (igual que CreditStatusSyncService::perform) para que el
                        //    CollectorSyncListener (síncrono) borre la fila de ruta.
                        CreditStatusSynced::dispatch($credit, $oldStatus, Credit::STATUS_PAID);

                        DB::commit();
                        $fixed++;

                        $this->line(sprintf(
                            '   ✓ Crédito #%d (empresa %d) %s → paid (%d cuota(s) corregida(s))',
                            $credit->id,
                            $credit->company_id,
                            $credit->getOriginal('status'),
                            $pendingInstallments->count()
                        ));
                    } catch (\Throwable $e) {
                        DB::rollBack();
                        $errors++;
                        $this->error("   ✗ Crédito #{$credit->id}: {$e->getMessage()}");
                    }
                }
            });

        // ── Resumen ──────────────────────────────────────────────────────────
        $this->newLine();

        if ($found === 0) {
            $this->info('✅ No se encontraron créditos con saldo $0 y estado incorrecto.');

            return self::SUCCESS;
        }

        $this->info("Créditos encontrados con saldo ≤ $0 y status incorrecto: {$found}");

        // Mostrar tabla (máximo 50 filas para no saturar la consola)
        $this->table(
            ['ID', 'Company', 'Status actual', 'Saldo', 'Cuotas'],
            array_slice($previewRows, 0, 50)
        );

        if (count($previewRows) > 50) {
            $this->comment('(Mostrando 50 de '.count($previewRows).' registros)');
        }

        $this->newLine();

        if ($apply) {
            $this->info("✅ Créditos corregidos  : {$fixed}");
            $this->info("   Cuotas corregidas    : {$installmentsFix}");

            if ($errors > 0) {
                $this->error("   Errores              : {$errors}");
                $this->comment('   Revisa los errores anteriores e intenta de nuevo.');
            }
        } else {
            $this->newLine();
            $this->comment('💡 Modo simulación — ningún dato fue modificado.');
            $this->comment('   Para corregir estos registros en producción:');
            $this->comment('');
            $this->comment('   php artisan credify:fix-completed-credits --apply');
        }

        $this->newLine();

        return $errors > 0 ? self::FAILURE : self::SUCCESS;
    }
}
