<?php

declare(strict_types=1);

namespace App\Services\Credits;

use App\DTOs\CreditOperationResult;
use App\DTOs\ExtendCreditData;
use App\DTOs\ExtendWithInterestData;
use App\DTOs\RefinanceCreditData;
use App\DTOs\RenewCreditData;
use App\DTOs\RestructureCreditData;
use App\Exceptions\CreditOperationException;
use App\Models\Credit;
use App\Services\Credits\Operations\ExtendCreditOperation;
use App\Services\Credits\Operations\ExtendWithInterestOperation;
use App\Services\Credits\Operations\RefinanceCreditOperation;
use App\Services\Credits\Operations\RenewCreditOperation;
use App\Services\Credits\Operations\RestructureCreditOperation;
use App\Support\CreditRules;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;

/**
 * Orquestador centralizado para operaciones estructurales de crédito.
 *
 * Responsabilidades:
 * - Validación de políticas (delegada a CreditOperationValidator)
 * - Manejo de transacciones
 * - Protección de idempotencia
 * - Logging y auditoría
 * - Delegación a operaciones específicas
 *
 * TODAS las operaciones estructurales (extender, refinanciar, reestructurar, renovar)
 * DEBEN pasar por este orquestador.
 */
class CreditOperationOrchestrator
{
    /**
     * Prefijo para locks de operaciones.
     */
    private const LOCK_PREFIX = 'credit_operation_lock:';

    /**
     * Tiempo de lock en segundos.
     */
    private const LOCK_TIMEOUT = 30;

    public function __construct(
        private readonly CreditOperationValidator $validator,
        private readonly ExtendCreditOperation $extendOperation,
        private readonly ExtendWithInterestOperation $extendWithInterestOperation,
        private readonly RefinanceCreditOperation $refinanceOperation,
        private readonly RestructureCreditOperation $restructureOperation,
        private readonly RenewCreditOperation $renewOperation,
    ) {}

    /**
     * EXTENSIÓN DE CRÉDITO
     *
     * Amplía el plazo de pago sin entregar dinero nuevo.
     * El saldo pendiente se redistribuye en más cuotas.
     *
     * @throws CreditOperationException
     */
    public function extend(ExtendCreditData $data): CreditOperationResult
    {
        return $this->executeWithProtection(
            creditId: $data->creditId,
            operationType: 'extension',
            operation: function (Credit $credit) use ($data) {
                // Validar políticas específicas de extensión
                $remainingBalance = $this->validator->validateForExtension($credit, $data);

                // Ejecutar operación
                return $this->extendOperation->execute($credit, $data, $remainingBalance);
            }
        );
    }

    /**
     * EXTENSIÓN DE CRÉDITO CON INTERESES ADICIONALES (Prórroga con Réditos)
     *
     * Extiende el plazo de un crédito que YA TIENE PAGOS DE CAPITAL,
     * aplicando intereses adicionales sobre el saldo pendiente.
     *
     * IMPORTANTE: Esta operación SÍ PERMITE créditos con pagos de capital.
     * Es la solución al problema de negocio real donde clientes con cuotas
     * pagadas necesitan extender su crédito.
     *
     * NO hay desembolso de efectivo.
     *
     * @throws CreditOperationException
     */
    public function extendWithInterest(ExtendWithInterestData $data): CreditOperationResult
    {
        return $this->executeWithProtection(
            creditId: $data->creditId,
            operationType: 'extension_with_interest',
            operation: function (Credit $credit) use ($data) {
                // Validar políticas específicas (SIN restricción de capital pagado)
                $remainingBalance = $this->validator->validateForExtensionWithInterest($credit, $data);

                // Ejecutar operación
                return $this->extendWithInterestOperation->execute($credit, $data, $remainingBalance);
            }
        );
    }

    /**
     * REFINANCIACIÓN DE CRÉDITO
     *
     * Crea nuevo crédito que incluye deuda anterior + capital adicional.
     * HAY DESEMBOLSO de efectivo (solo el capital adicional).
     *
     * @throws CreditOperationException
     */
    public function refinance(RefinanceCreditData $data): CreditOperationResult
    {
        return $this->executeWithProtection(
            creditId: $data->creditId,
            operationType: 'refinance',
            operation: function (Credit $credit) use ($data) {
                // Validar políticas específicas de refinanciación
                $remainingBalance = $this->validator->validateForRefinance($credit, $data);

                // Ejecutar operación
                return $this->refinanceOperation->execute($credit, $data, $remainingBalance);
            }
        );
    }

    /**
     * REESTRUCTURACIÓN DE CRÉDITO
     *
     * Modifica condiciones del crédito sin dinero nuevo.
     * Puede capitalizar intereses vencidos.
     *
     * @throws CreditOperationException
     */
    public function restructure(RestructureCreditData $data): CreditOperationResult
    {
        return $this->executeWithProtection(
            creditId: $data->creditId,
            operationType: 'restructure',
            operation: function (Credit $credit) use ($data) {
                // Validar políticas específicas de reestructuración
                $remainingBalance = $this->validator->validateForRestructure($credit, $data);

                // Ejecutar operación
                return $this->restructureOperation->execute($credit, $data, $remainingBalance);
            }
        );
    }

    /**
     * RENOVACIÓN DE CRÉDITO
     *
     * Nuevo crédito que paga el anterior y entrega la diferencia.
     * Requiere que el cliente haya realizado pagos previos.
     *
     * @throws CreditOperationException
     */
    public function renew(RenewCreditData $data): CreditOperationResult
    {
        return $this->executeWithProtection(
            creditId: $data->creditId,
            operationType: 'renewal',
            operation: function (Credit $credit) use ($data) {
                // Validar políticas específicas de renovación
                $remainingBalance = $this->validator->validateForRenewal($credit, $data);

                // Ejecutar operación
                return $this->renewOperation->execute($credit, $data, $remainingBalance);
            }
        );
    }

    /**
     * Ejecuta una operación con todas las protecciones necesarias.
     *
     * 1. Lock de idempotencia
     * 2. Transacción DB
     * 3. Lock FOR UPDATE del crédito
     * 4. Logging estructurado
     */
    private function executeWithProtection(
        int $creditId,
        string $operationType,
        callable $operation,
    ): CreditOperationResult {
        $lockKey = self::LOCK_PREFIX.$creditId;

        // 1. Adquirir lock de idempotencia
        $lock = Cache::lock($lockKey, self::LOCK_TIMEOUT);

        if (! $lock->get()) {
            throw CreditOperationException::duplicateOperation($creditId, $operationType);
        }

        try {
            // 2. Ejecutar en transacción DB
            return DB::transaction(function () use ($creditId, $operationType, $operation) {

                // 3. Obtener crédito con lock FOR UPDATE
                $credit = Credit::where('id', $creditId)
                    ->lockForUpdate()
                    ->first();

                if (! $credit) {
                    throw new CreditOperationException(
                        "Crédito #{$creditId} no encontrado.",
                        CreditOperationException::CODE_CREDIT_NOT_FOUND,
                        ['credit_id' => $creditId]
                    );
                }

                // 4. Logging de inicio
                Log::info('Iniciando operación de crédito', [
                    'operation' => $operationType,
                    'credit_id' => $creditId,
                    'user_id' => auth()->id(),
                    'company_id' => $credit->company_id,
                ]);

                // 5. Ejecutar operación
                $result = $operation($credit);

                // 6. Logging de éxito
                Log::info('Operación de crédito completada', [
                    'operation' => $operationType,
                    'credit_id' => $creditId,
                    'new_credit_id' => $result->newCredit->id,
                    'user_id' => auth()->id(),
                    'financial_summary' => [
                        'previous_balance' => $result->previousBalance->value(),
                        'new_principal' => $result->newPrincipal->value(),
                        'cash_out' => $result->cashOut->value(),
                    ],
                ]);

                return $result;
            });
        } catch (CreditOperationException $e) {
            // Re-lanzar excepciones de negocio
            Log::warning('Operación de crédito rechazada', [
                'operation' => $operationType,
                'credit_id' => $creditId,
                'error_code' => $e->getErrorCode(),
                'message' => $e->getMessage(),
                'context' => $e->getContext(),
            ]);
            throw $e;
        } catch (\Throwable $e) {
            // Logging de error inesperado
            Log::error('Error en operación de crédito', [
                'operation' => $operationType,
                'credit_id' => $creditId,
                'error' => $e->getMessage(),
                'trace' => $e->getTraceAsString(),
            ]);
            throw $e;
        } finally {
            // 7. Liberar lock
            $lock->release();
        }
    }

    /**
     * Verifica si una operación puede ejecutarse sin ejecutarla.
     * Útil para UI (mostrar/ocultar botones).
     *
     * IMPORTANTE: Verifica que el crédito NO sea padre (debe ser leaf credit).
     */
    public function canExtend(Credit $credit): bool
    {
        try {
            $this->validator->assertCanOperate($credit);
            $this->validator->assertValidState($credit);
            $this->validator->assertNotParent($credit);
            $this->validator->assertNoCapitalPaid($credit);
            $this->validator->assertHasBalance($credit);

            return true;
        } catch (CreditOperationException) {
            return false;
        }
    }

    /**
     * Verifica si un crédito puede ser extendido CON intereses adicionales.
     *
     * IMPORTANTE: Esta verificación NO usa assertNoCapitalPaid(),
     * permitiendo la operación en créditos que ya tienen pagos de capital.
     *
     * Esta es la operación correcta para el caso de negocio real donde
     * un cliente con cuotas pagadas necesita extender su crédito.
     */
    public function canExtendWithInterest(Credit $credit): bool
    {
        return $this->validator->canExtendWithInterest($credit);
    }

    public function canRefinance(Credit $credit): bool
    {
        try {
            $this->validator->assertCanOperate($credit);
            $this->validator->assertValidState($credit);
            $this->validator->assertNotParent($credit);
            // #117: refinance permite créditos con capital pagado (préstamo nuevo, tasa consentida)
            $this->validator->assertHasBalance($credit);

            return true;
        } catch (CreditOperationException) {
            return false;
        }
    }

    public function canRestructure(Credit $credit): bool
    {
        try {
            $this->validator->assertCanOperate($credit);
            $this->validator->assertValidState($credit);
            $this->validator->assertNotParent($credit);
            // #117: restructure permite créditos con capital pagado (el usuario controla la tasa; default 0)
            $this->validator->assertHasBalance($credit);

            return true;
        } catch (CreditOperationException) {
            return false;
        }
    }

    public function canRenew(Credit $credit): bool
    {
        try {
            $this->validator->assertCanOperate($credit);
            $this->validator->assertValidState($credit);
            $this->validator->assertNotParent($credit);
            // #95: la renovación permite créditos con capital pagado (no llama assertNoCapitalPaid)
            $this->validator->assertHasBalance($credit);

            // Renovación REQUIERE pagos previos
            if (! CreditRules::hasPayments($credit)) {
                return false;
            }

            return true;
        } catch (CreditOperationException) {
            return false;
        }
    }
}
