<?php

namespace App\Services\Credits;

use App\Models\Credit;
use App\Models\CreditAuditLog;
use App\Models\Installment;
use App\Models\User;
use App\Services\CreditStatusSyncService;
use App\Services\Payments\PaymentMaterializer;
use App\ValueObjects\Money;
use Carbon\Carbon;
use Exception;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;

/**
 * Servicio para reprogramar cuotas de crédito.
 *
 * Usa Money Value Object con BC Math para precisión financiera exacta.
 */
class CreditInstallmentRescheduler
{
    /**
     * Reajusta el plan de cuotas (montos + fechas) reemplazando installments.
     *
     * Reglas duras:
     * - Multi-tenant estricto (SuperAdmin NO ejecuta operaciones).
     * - PROHIBIDO si existe cualquier pago aplicado (pivot applied_amount > 0 en pagos no voided).
     * - Se mantiene proporción global principal/interés del plan original.
     * - Auditoría inmutable (evento compensatorio, no overwrite).
     *
     * @param  array<int, array{due_date:string, amount:float|int|string}>  $newSchedule
     */
    public function reschedule(Credit $credit, array $newSchedule, ?string $reason = null): Credit
    {
        $user = Auth::user();
        if (! $user) {
            throw new Exception('Usuario no autenticado.');
        }

        // SuperAdmin: lectura global, NO operar (según regla fundamental)
        if (method_exists($user, 'isSuperAdmin') && $user->isSuperAdmin()) {
            throw new Exception('SuperAdmin no está autorizado para ejecutar reajustes.');
        }

        $this->assertSameCompany($credit, $user);

        return DB::transaction(function () use ($credit, $newSchedule, $reason, $user) {

            // 1) Lock del crédito
            $credit = Credit::where('id', $credit->id)
                ->where('company_id', $credit->company_id)
                ->lockForUpdate()
                ->firstOrFail();

            // 2) Lock de cuotas actuales (si existen) para estabilidad
            $currentInstallments = Installment::where('credit_id', $credit->id)
                ->orderBy('installment_number')
                ->lockForUpdate()
                ->get();

            if ($currentInstallments->isEmpty()) {
                throw new Exception('El crédito no tiene cuotas para reajustar.');
            }

            // 3) Política A dura para reajuste estructural: no puede existir ningún pago aplicado
            if ($this->hasAnyAppliedPayments($credit)) {
                throw new Exception('No se puede reajustar: el crédito ya tiene pagos aplicados (Política Financiera A).');
            }

            // 4) Snapshot BEFORE (auditoría)
            $before = [
                'credit_id' => $credit->id,
                'installments' => $currentInstallments->map(fn (Installment $i) => [
                    'id' => $i->id,
                    'installment_number' => $i->installment_number,
                    'status' => $i->status,
                    'due_date' => optional($i->due_date)->toDateString(),
                    'paid_date' => optional($i->paid_date)->toDateString(),
                    'principal_amount' => Money::of($i->principal_amount)->value(),
                    'interest_amount' => Money::of($i->interest_amount)->value(),
                    'total_amount' => Money::of($i->total_amount)->value(),
                    'principal_balance_after' => Money::of($i->principal_balance_after)->value(),
                    'amount_paid' => Money::of($i->amount_paid)->value(),
                ])->values()->all(),
            ];

            // 5) Validación + normalización de schedule de entrada
            $normalized = $this->normalizeAndValidateNewSchedule($newSchedule);

            // 6) Total objetivo usando Money para precisión
            $oldTotal = Money::zero();
            foreach ($currentInstallments as $inst) {
                $oldTotal = $oldTotal->add(Money::of($inst->total_amount));
            }

            $newTotal = Money::zero();
            foreach ($normalized as $row) {
                $newTotal = $newTotal->add(Money::of($row['total_amount']));
            }

            if (! $newTotal->equals($oldTotal)) {
                throw new Exception("La suma de las nuevas cuotas ({$newTotal->value()}) debe ser EXACTAMENTE igual al total actual ({$oldTotal->value()}).");
            }

            // 7) Mantener proporción global principal/interés del plan original
            $oldPrincipal = Money::zero();
            $oldInterest = Money::zero();
            foreach ($currentInstallments as $inst) {
                $oldPrincipal = $oldPrincipal->add(Money::of($inst->principal_amount));
                $oldInterest = $oldInterest->add(Money::of($inst->interest_amount));
            }

            if ($oldTotal->isZero() || $oldTotal->isNegative()) {
                throw new Exception('El total actual del plan es inválido para recalcular proporción.');
            }

            // Calcular ratio usando BC Math (alta precisión interna)
            $principalRatio = bcdiv($oldPrincipal->raw(), $oldTotal->raw(), 8);

            // 8) Reemplazo atómico del schedule
            Installment::where('credit_id', $credit->id)->delete();

            $remainingPrincipal = Money::of($credit->amount);
            $created = [];

            foreach ($normalized as $index => $row) {
                $total = Money::of($row['total_amount']);

                // principal por proporción global
                $principal = $total->multiply($principalRatio);
                $interest = $total->subtract($principal);

                // principal_balance_after: saldo restante de principal tras esta cuota
                $remainingPrincipal = $remainingPrincipal->subtract($principal)->max(0);

                $inst = Installment::create([
                    'company_id' => $credit->company_id,
                    'credit_id' => $credit->id,
                    'installment_number' => $index + 1,
                    'status' => Installment::STATUS_PENDING,
                    'due_date' => $row['due_date'],
                    'paid_date' => null,
                    'principal_amount' => $principal->value(),
                    'interest_amount' => $interest->value(),
                    'total_amount' => $total->value(),
                    'principal_balance_after' => $remainingPrincipal->value(),
                    'amount_paid' => 0,
                ]);

                $created[] = [
                    'id' => $inst->id,
                    'installment_number' => $inst->installment_number,
                    'status' => $inst->status,
                    'due_date' => optional($inst->due_date)->toDateString(),
                    'principal_amount' => Money::of($inst->principal_amount)->value(),
                    'interest_amount' => Money::of($inst->interest_amount)->value(),
                    'total_amount' => Money::of($inst->total_amount)->value(),
                    'principal_balance_after' => Money::of($inst->principal_balance_after)->value(),
                    'amount_paid' => Money::of($inst->amount_paid)->value(),
                ];
            }

            // 9) Materialización (deja estado consistente aunque no haya pagos)
            app(PaymentMaterializer::class)->materializeCredit($credit->fresh());

            // 10) Auditoría inmutable (evento compensatorio)
            CreditAuditLog::create([
                'company_id' => $credit->company_id,
                'credit_id' => $credit->id,
                'user_id' => $user->id,
                'action' => 'installments_rescheduled',
                'data' => [
                    'reason' => $reason,
                    'before' => $before,
                    'after' => [
                        'credit_id' => $credit->id,
                        'installments' => $created,
                    ],
                    'ratios' => [
                        'principal_ratio' => (float) $principalRatio,
                        'old_total' => $oldTotal->value(),
                        'old_principal' => $oldPrincipal->value(),
                        'old_interest' => $oldInterest->value(),
                    ],
                ],
            ]);

            // 11) Sync de estado: reprogramar fechas puede cambiar estado (DELAYED→ACTIVE)
            app(CreditStatusSyncService::class)->sync($credit);

            return $credit->fresh(['installments']);
        });
    }

    public function canReschedule(Credit $credit): bool
    {
        $user = Auth::user();
        if (! $user) {
            return false;
        }

        if (method_exists($user, 'isSuperAdmin') && $user->isSuperAdmin()) {
            return false;
        }

        if ((int) $user->company_id !== (int) $credit->company_id) {
            return false;
        }

        return ! $this->hasAnyAppliedPayments($credit);
    }

    private function assertSameCompany(Credit $credit, User $user): void
    {
        if ((int) $user->company_id !== (int) $credit->company_id) {
            throw new Exception('No está autorizado para operar sobre este crédito.');
        }
    }

    /**
     * Condición dura: existe cualquier pivot aplicado en pagos no voided.
     */
    private function hasAnyAppliedPayments(Credit $credit): bool
    {
        return DB::table('payment_installment as pi')
            ->join('payments as p', 'p.id', '=', 'pi.payment_id')
            ->join('installments as i', 'i.id', '=', 'pi.installment_id')
            ->where('i.credit_id', $credit->id)
            ->where('p.company_id', $credit->company_id)
            ->where('p.voided', 0)
            ->where('pi.applied_amount', '>', 0)
            ->exists();
    }

    /**
     * @param  array<int, array{due_date:string, amount:float|int|string}>  $newSchedule
     * @return array<int, array{due_date:string, total_amount:float}>
     */
    private function normalizeAndValidateNewSchedule(array $newSchedule): array
    {
        if (empty($newSchedule)) {
            throw new Exception('Debe ingresar al menos una cuota.');
        }

        $rows = [];

        foreach ($newSchedule as $idx => $row) {
            $due = $row['due_date'] ?? null;
            $amt = $row['amount'] ?? null;

            if (! $due) {
                throw new Exception('La cuota #'.($idx + 1).' no tiene due_date.');
            }
            if ($amt === null || $amt === '') {
                throw new Exception('La cuota #'.($idx + 1).' no tiene amount.');
            }

            $dueDate = Carbon::parse($due)->toDateString();
            $amount = Money::of($amt);

            if ($amount->isZero() || $amount->isNegative()) {
                throw new Exception('La cuota #'.($idx + 1).' debe ser mayor a 0.');
            }

            $rows[] = [
                'due_date' => $dueDate,
                'total_amount' => $amount->value(),
            ];
        }

        // Orden estable por fecha (y luego por orden de entrada)
        usort($rows, function ($a, $b) {
            return strcmp($a['due_date'], $b['due_date']);
        });

        return $rows;
    }
}
