<?php

require_once __DIR__ . '/DocumentManager.php';
require_once __DIR__ . '/ColorMapper.php';
require_once __DIR__ . '/SMSService.php';
require_once __DIR__ . '/URLShortener.php';

class TransactionManager {
    private $db;
    private $masteroneDb;
    private $logger;
    private $dominicanaAPI;
    private $documentManager;
    private $smsService;
    private $urlShortener;

    public function __construct() {
        $this->db = new Database();
        $this->masteroneDb = new MasteroneDatabase();
        $this->logger = new Logger();
        $this->dominicanaAPI = new DominicanaAPI();
        $this->documentManager = new DocumentManager();
        $this->smsService = new SMSService();
        $this->urlShortener = new URLShortener();
    }
    
    public function iniciarTransaccion($datos) {
        try {
            // Generar UUID único para la transacción
            $uuid = $this->generateUUID();

            // El plan es opcional en el nuevo flujo: se selecciona después de confirmar la versión del vehículo.
            // Se mantiene compatibilidad con el flujo antiguo si tipo_cobertura viene en la petición.
            $cdTipoPlan = null;
            $plan = null;
            if (!empty($datos['tipo_cobertura'])) {
                $cdTipoPlan = (int)$datos['tipo_cobertura'];
                $plan = $this->getPlanInfo($cdTipoPlan);
                if (!$plan) {
                    throw new Exception('Plan de seguro no encontrado');
                }
            }

            $conn = $this->db->connect();
            $stmt = $conn->prepare("
                INSERT INTO transacciones
                (uuid, disashop_id, cedula, telefono, placa, cd_tipo_plan,
                 incluye_grua, incluye_casa_conductor, monto_total, ip_cliente, user_agent, estado)
                VALUES (?, ?, ?, ?, ?, ?, ?, ?, 0, ?, ?, 'iniciada')
            ");

            $stmt->execute([
                $uuid,
                $datos['disashop_id'] ?? null,
                $datos['cedula'],
                $datos['telefono'],
                $datos['placa'],
                $cdTipoPlan,
                isset($datos['incluye_grua']) && $datos['incluye_grua'] ? 1 : 0,
                isset($datos['incluye_casa_conductor']) && $datos['incluye_casa_conductor'] ? 1 : 0,
                $_SERVER['REMOTE_ADDR'] ?? '',
                $_SERVER['HTTP_USER_AGENT'] ?? ''
            ]);

            $transactionId = $conn->lastInsertId();

            $this->logger->info('Nueva transacción iniciada', [
                'transaction_id' => $transactionId,
                'uuid'           => $uuid,
                'cd_tipo_plan'   => $cdTipoPlan ?? 'pendiente'
            ], $transactionId);

            return [
                'transaction_id' => $transactionId,
                'uuid'           => $uuid,
                'plan'           => $plan  // null cuando se usa el flujo nuevo
            ];
        } catch (Exception $e) {
            $this->logger->error('Error iniciando transacción', [
                'error' => $e->getMessage(),
                'datos' => $datos
            ]);
            throw $e;
        }
    }
    
    public function validarDatos($transactionId, $cedula, $placa) {
        try {
            // Validar cédula en masterone
            $individuo = $this->validarCedula($cedula);
            if (!$individuo) {
                throw new Exception('Cédula no encontrada en el sistema');
            }

            // Validar placa en masterone
            $vehiculo = $this->validarPlaca($placa);
            if (!$vehiculo) {
                throw new Exception('Placa no encontrada en el sistema');
            }

            // Leer el tipo de vehículo de MasterOne (campo 'tipo') y mapearlo localmente
            $tipoVehiculoMasterone = $vehiculo['tipo'] ?? null;
            $tipoVehiculoMapped    = $this->mapearTipoVehiculoMasterone($tipoVehiculoMasterone);

            $this->logger->info('Vehículo detectado en MasterOne', [
                'transaction_id'          => $transactionId,
                'marca'                   => $vehiculo['marca'],
                'modelo'                  => $vehiculo['modelo'],
                'tipo_masterone'          => $tipoVehiculoMasterone,
                'tipo_vehiculo_mapped'    => $tipoVehiculoMapped
            ], $transactionId);

            // Actualizar transacción con los datos validados
            $conn = $this->db->connect();
            $stmt = $conn->prepare("
                UPDATE transacciones
                SET nombre_completo = ?, marca = ?, modelo = ?, año = ?, chasis = ?,
                    color = ?, tipo_vehiculo_masterone = ?, estado = 'datos_validados'
                WHERE id = ?
            ");

            $stmt->execute([
                $individuo['nombre_completo'],
                $vehiculo['marca'],
                $vehiculo['modelo'],
                $vehiculo['ano'],
                $vehiculo['chasis'],
                $vehiculo['color'] ?? null,
                $tipoVehiculoMasterone,
                $transactionId
            ]);

            // Verificar si el vehículo tiene una póliza pendiente de renovación en DCS
            $esRenovacion    = false;
            $datosRenovacion = null;
            $montoRenovacion = null;
            try {
                $prerenovaciones = $this->dominicanaAPI->getPrerenovacionesAsegurado($cedula);
                // La respuesta puede ser un array directo o tener una clave de contenido
                $lista = [];
                if (isset($prerenovaciones['content'])) {
                    $lista = $prerenovaciones['content'];
                } elseif (isset($prerenovaciones['prerenovaciones'])) {
                    $lista = $prerenovaciones['prerenovaciones'];
                } elseif (is_array($prerenovaciones) && !isset($prerenovaciones['nuPoliza'])) {
                    $lista = $prerenovaciones;
                } elseif (isset($prerenovaciones['nuPoliza'])) {
                    $lista = [$prerenovaciones];
                }

                $placaNorm = strtoupper(trim($placa));
                foreach ($lista as $renov) {
                    $placaRenov = strtoupper(trim($renov['placa'] ?? ''));
                    if ($placaRenov === $placaNorm) {
                        $esRenovacion    = true;
                        $datosRenovacion = $renov;
                        $montoRenovacion = isset($renov['montoPrima']) ? (float)$renov['montoPrima']
                                         : (isset($renov['prima'])      ? (float)$renov['prima'] : null);
                        break;
                    }
                }

                if ($esRenovacion) {
                    $this->logger->info('Renovación pendiente encontrada en DCS', [
                        'transaction_id'   => $transactionId,
                        'placa'            => $placa,
                        'nu_poliza'        => $datosRenovacion['nuPoliza'] ?? 'N/A',
                        'monto_renovacion' => $montoRenovacion
                    ], $transactionId);

                    $connR = $this->db->connect();
                    $stmtR = $connR->prepare("
                        UPDATE transacciones
                        SET es_renovacion = 1, datos_renovacion = ?, monto_total = ?, updated_at = NOW()
                        WHERE id = ?
                    ");
                    $stmtR->execute([
                        json_encode($datosRenovacion),
                        $montoRenovacion,
                        $transactionId
                    ]);
                }
            } catch (Exception $eRenov) {
                // La verificación de renovación no bloquea el flujo principal
                $this->logger->warning('No se pudo verificar renovación en DCS (no crítico)', [
                    'error'          => $eRenov->getMessage(),
                    'transaction_id' => $transactionId
                ], $transactionId);
            }

            $this->logger->info('Datos validados correctamente', [
                'transaction_id' => $transactionId,
                'cedula'         => $cedula,
                'placa'          => $placa
            ], $transactionId);

            $result = [
                'individuo' => [
                    'nombre_oculto' => $this->ocultarNombre($individuo['nombre_completo'])
                ],
                'vehiculo' => [
                    'descripcion_oculta'   => $this->ocultarVehiculo($vehiculo['marca'], $vehiculo['modelo']),
                    'tipo_vehiculo'        => $tipoVehiculoMapped,
                    'tipo_vehiculo_raw'    => $tipoVehiculoMasterone
                ],
                'tiene_renovacion' => $esRenovacion
            ];

            if ($esRenovacion && $datosRenovacion) {
                $result['renovacion'] = [
                    'nu_poliza'      => $datosRenovacion['nuPoliza']      ?? null,
                    'nu_mes'         => $datosRenovacion['nuMes']         ?? null,
                    'nu_annio'       => $datosRenovacion['nuAnnio']       ?? null,
                    'monto_total'    => $montoRenovacion,
                    'marca'          => $datosRenovacion['marca']         ?? null,
                    'modelo'         => $datosRenovacion['modelo']        ?? null,
                    'version'        => $datosRenovacion['version']       ?? null,
                    'vigencia_hasta' => $datosRenovacion['vigenciaHasta'] ?? $datosRenovacion['fechaHasta'] ?? null,
                ];
            }

            return $result;
        } catch (Exception $e) {
            $this->logger->error('Error validando datos', [
                'error'          => $e->getMessage(),
                'transaction_id' => $transactionId
            ], $transactionId);
            throw $e;
        }
    }
    
    public function enviarOTP($transactionId) {
        try {
            $transaccion = $this->getTransaccion($transactionId);
            if (!$transaccion) {
                throw new Exception('Transacción no encontrada');
            }
            
            // Generar código OTP
            $codigo = $this->generarCodigoOTP();
            $expiraAt = date('Y-m-d H:i:s', strtotime('+' . OTP_EXPIRATION_MINUTES . ' minutes'));
            
            // Guardar OTP en la base de datos
            $conn = $this->db->connect();
            $stmt = $conn->prepare("
                INSERT INTO otp_validaciones (transaccion_id, codigo, telefono, expira_at)
                VALUES (?, ?, ?, ?)
            ");
            $stmt->execute([$transactionId, $codigo, $transaccion['telefono'], $expiraAt]);

            // Enviar SMS con código OTP usando BroadcasterMobile
            try {
                $this->smsService->enviarOTP($transaccion['telefono'], $codigo);
            } catch (Exception $e) {
                $this->logger->warning('Error al enviar SMS OTP (continuando)', [
                    'error' => $e->getMessage(),
                    'transaction_id' => $transactionId
                ], $transactionId);
            }

            // Actualizar estado de la transacción
            $stmt = $conn->prepare("UPDATE transacciones SET estado = 'otp_enviado' WHERE id = ?");
            $stmt->execute([$transactionId]);
            
            $this->logger->info('OTP enviado', [
                'transaction_id' => $transactionId,
                'telefono' => $transaccion['telefono']
            ], $transactionId);
            
            // TODO: REMOVER antes de producción — código visible solo para pruebas de Disashop
            return [
                'mensaje' => 'Código OTP enviado correctamente',
                'codigo_prueba' => $codigo  // TEMPORAL: remover en producción
            ];
        } catch (Exception $e) {
            $this->logger->error('Error enviando OTP', [
                'error' => $e->getMessage(),
                'transaction_id' => $transactionId
            ], $transactionId);
            throw $e;
        }
    }
    
    public function validarOTP($transactionId, $codigo) {
        try {
            $conn = $this->db->connect();
            
            // Buscar el código OTP
            $stmt = $conn->prepare("
                SELECT * FROM otp_validaciones 
                WHERE transaccion_id = ? AND codigo = ? AND validado = 0 AND expira_at > NOW()
                ORDER BY created_at DESC LIMIT 1
            ");
            $stmt->execute([$transactionId, $codigo]);
            $otp = $stmt->fetch();
            
            if (!$otp) {
                // Incrementar intentos fallidos
                $stmt = $conn->prepare("
                    UPDATE otp_validaciones 
                    SET intentos = intentos + 1 
                    WHERE transaccion_id = ? AND codigo = ?
                ");
                $stmt->execute([$transactionId, $codigo]);
                
                throw new Exception('Código OTP inválido o expirado');
            }
            
            // Validar el OTP
            $stmt = $conn->prepare("
                UPDATE otp_validaciones 
                SET validado = 1, validado_at = NOW() 
                WHERE id = ?
            ");
            $stmt->execute([$otp['id']]);
            
            // Actualizar estado de la transacción
            $stmt = $conn->prepare("UPDATE transacciones SET estado = 'otp_validado' WHERE id = ?");
            $stmt->execute([$transactionId]);
            
            $this->logger->info('OTP validado correctamente', [
                'transaction_id' => $transactionId
            ], $transactionId);
            
            return ['mensaje' => 'Código OTP validado correctamente'];
        } catch (Exception $e) {
            $this->logger->error('Error validando OTP', [
                'error' => $e->getMessage(),
                'transaction_id' => $transactionId
            ], $transactionId);
            throw $e;
        }
    }
    
    public function procesarPago($transactionId) {
        try {
            $transaccion = $this->getTransaccion($transactionId);
            // Aceptar 'version_confirmada' (flujo nuevo) u 'otp_validado' (flujo legado)
            $estadosValidos = ['otp_validado', 'version_confirmada'];
            if (!$transaccion || !in_array($transaccion['estado'], $estadosValidos)) {
                throw new Exception('Transacción no válida para procesar pago. Estado: ' . ($transaccion['estado'] ?? 'N/A'));
            }
            
            // Actualizar estado de la transacción
            $conn = $this->db->connect();
            $stmt = $conn->prepare("UPDATE transacciones SET estado = 'pago_procesado' WHERE id = ?");
            $stmt->execute([$transactionId]);
            
            $this->logger->info('Pago procesado', [
                'transaction_id' => $transactionId,
                'monto' => $transaccion['monto_total']
            ], $transactionId);
            
            return ['mensaje' => 'Pago procesado correctamente'];
        } catch (Exception $e) {
            $this->logger->error('Error procesando pago', [
                'error' => $e->getMessage(),
                'transaction_id' => $transactionId
            ], $transactionId);
            throw $e;
        }
    }
    
    public function obtenerCotizacionDCS($transactionId) {
        try {
            $transaccion = $this->getTransaccionCompleta($transactionId);

            if (!$transaccion || !in_array($transaccion['estado'], ['otp_validado', 'datos_validados', 'version_confirmada'])) {
                throw new Exception('Estado de transacción no válido para obtener cotización. Estado: ' . ($transaccion['estado'] ?? 'N/A'));
            }

            // Si ya tenemos cotización guardada, devolverla sin llamar a DCS de nuevo
            if (!empty($transaccion['numero_cotizacion'])) {
                return [
                    'numero_cotizacion' => $transaccion['numero_cotizacion'],
                    'monto_total'       => (float)$transaccion['monto_total'],
                    'prima'             => (float)$transaccion['monto_total']
                ];
            }

            // Crear o buscar persona en Dominicana de Seguros
            $personaExiste = false;
            try {
                $this->dominicanaAPI->getPerson($transaccion['cedula']);
                $personaExiste = true;
            } catch (Exception $e) {
                $nombreParts = explode(' ', trim($transaccion['nombre_completo']));
                $totalParts  = count($nombreParts);
                $personData  = [
                    'cedula'           => $transaccion['cedula'],
                    'primer_nombre'    => $nombreParts[0] ?? '',
                    'segundo_nombre'   => $totalParts > 3 ? $nombreParts[1] : ($nombreParts[1] ?? ''),
                    'primer_apellido'  => $totalParts > 3 ? $nombreParts[$totalParts - 2] : ($nombreParts[2] ?? ''),
                    'segundo_apellido' => $totalParts > 3 ? $nombreParts[$totalParts - 1] : ($nombreParts[3] ?? ''),
                    'telefono'         => $transaccion['telefono'],
                    'sexo'             => 'M'
                ];
                $this->dominicanaAPI->createPerson($personData);
                try {
                    $this->dominicanaAPI->getPerson($transaccion['cedula']);
                    $personaExiste = true;
                } catch (Exception $verifyError) {
                    throw new Exception('La persona fue creada pero no se puede verificar: ' . $verifyError->getMessage());
                }
            }

            if (!$personaExiste) {
                throw new Exception('No se pudo crear o verificar la persona en el sistema de Dominicana');
            }

            // Llamar procesarVenta para obtener cotización y prima real de DCS
            // Nota: casaConductor y asistenciaVial van dentro de datosVehiculoRequestList (600006/600007)
            $ventaData = [
                'nuDocumento'              => $transaccion['cedula'],
                'cdTipoPlan'               => $this->mapearTipoPlan($transaccion),
                'datosVehiculoRequestList' => $this->prepararDatosVehiculo($transaccion)
            ];

            try {
                $ventaResponse = $this->dominicanaAPI->procesarVenta($ventaData);
            } catch (Exception $e) {
                $this->_detectarPolizaActiva($e, $transactionId);
                throw $e;
            }

            if (!isset($ventaResponse['nuCotizacion'])) {
                throw new Exception('Error en procesarVenta: nuCotizacion no presente en respuesta');
            }

            $prima = isset($ventaResponse['prima']) ? (float)$ventaResponse['prima'] : 0;

            // Guardar nuCotizacion y actualizar monto_total con la prima real de DCS
            $conn = $this->db->connect();
            $conn->prepare("UPDATE transacciones SET numero_cotizacion = ?, monto_total = ?, updated_at = NOW() WHERE id = ?")
                 ->execute([$ventaResponse['nuCotizacion'], $prima, $transaccion['id']]);

            $this->logger->info('Cotización DCS obtenida y guardada', [
                'transaction_id'    => $transactionId,
                'numero_cotizacion' => $ventaResponse['nuCotizacion'],
                'prima'             => $prima
            ], $transactionId);

            return [
                'numero_cotizacion' => $ventaResponse['nuCotizacion'],
                'monto_total'       => $prima,
                'prima'             => $prima
            ];
        } catch (Exception $e) {
            $this->logger->error('Error obteniendo cotización DCS', [
                'error'          => $e->getMessage(),
                'transaction_id' => $transactionId
            ], $transactionId);
            throw $e;
        }
    }

    public function emitirPoliza($transactionId) {
        try {
            $transaccion = $this->getTransaccionCompleta($transactionId);

            $this->logger->info('[ETAPA: emitirPoliza] Iniciando emisión', [
                'transaction_id' => $transactionId,
                'estado_actual'  => $transaccion['estado'] ?? 'NO_ENCONTRADA',
                'cedula'         => $transaccion['cedula'] ?? 'N/A',
                'placa'          => $transaccion['placa'] ?? 'N/A',
                'marca'          => $transaccion['marca'] ?? 'N/A',
                'modelo'         => $transaccion['modelo'] ?? 'N/A',
                'cd_tipo_plan'   => $transaccion['cd_tipo_plan'] ?? 'N/A'
            ], $transactionId);

            if (!$transaccion || $transaccion['estado'] !== 'pago_procesado') {
                $this->logger->error('[ETAPA: emitirPoliza] Estado inválido para emitir', [
                    'transaction_id' => $transactionId,
                    'estado_actual'  => $transaccion['estado'] ?? 'NO_ENCONTRADA',
                    'requerido'      => 'pago_procesado'
                ], $transactionId);
                throw new Exception('Transacción no válida para emitir póliza. Estado actual: ' . ($transaccion['estado'] ?? 'NO_ENCONTRADA'));
            }

            // ── Flujo de RENOVACIÓN ───────────────────────────────────────────────────
            if (!empty($transaccion['es_renovacion'])) {
                return $this->_ejecutarRenovacion($transaccion);
            }

            // Si ya tenemos cotización DCS previa (obtenida en /get-quote), usarla directamente
            $cotizacionPrevia = !empty($transaccion['numero_cotizacion']);
            if ($cotizacionPrevia) {
                $ventaResponse = [
                    'nuCotizacion' => $transaccion['numero_cotizacion'],
                    'prima'        => (float)$transaccion['monto_total']
                ];
                $this->logger->info('[ETAPA: emitirPoliza] Usando cotización DCS previamente obtenida', [
                    'transaction_id'    => $transactionId,
                    'numero_cotizacion' => $transaccion['numero_cotizacion']
                ], $transactionId);
            } else {
            // Crear o buscar persona en Dominicana de Seguros
            $personaExiste = false;

            try {
                $persona = $this->dominicanaAPI->getPerson($transaccion['cedula']);
                $personaExiste = true;
                $this->logger->info('Persona encontrada en Dominicana', [
                    'cedula' => $transaccion['cedula'],
                    'persona' => $persona
                ], $transaccion['id']);
            } catch (Exception $e) {
                // Si no existe, crearla
                $this->logger->info('Persona no encontrada, intentando crear', [
                    'cedula' => $transaccion['cedula'],
                    'error_get' => $e->getMessage()
                ], $transaccion['id']);

                // Dividir el nombre correctamente
                $nombreParts = explode(' ', trim($transaccion['nombre_completo']));
                $totalParts = count($nombreParts);

                // Asignar nombres de forma inteligente
                // Asumimos: primeros dos son nombres, últimos dos son apellidos
                $personData = [
                    'cedula' => $transaccion['cedula'],
                    'primer_nombre' => $nombreParts[0] ?? '',
                    'segundo_nombre' => $totalParts > 3 ? $nombreParts[1] : ($nombreParts[1] ?? ''),
                    'primer_apellido' => $totalParts > 3 ? $nombreParts[$totalParts - 2] : ($nombreParts[2] ?? ''),
                    'segundo_apellido' => $totalParts > 3 ? $nombreParts[$totalParts - 1] : ($nombreParts[3] ?? ''),
                    'telefono' => $transaccion['telefono'],
                    'sexo' => 'M'
                ];

                $this->logger->info('Datos de persona a crear', [
                    'person_data' => $personData
                ], $transaccion['id']);

                // Intentar crear la persona
                $createResult = $this->dominicanaAPI->createPerson($personData);

                $this->logger->info('Persona creada exitosamente', [
                    'cedula' => $transaccion['cedula'],
                    'result' => $createResult
                ], $transaccion['id']);

                // Verificar que ahora existe
                try {
                    $persona = $this->dominicanaAPI->getPerson($transaccion['cedula']);
                    $personaExiste = true;
                    $this->logger->info('Persona verificada después de crear', [
                        'cedula' => $transaccion['cedula']
                    ], $transaccion['id']);
                } catch (Exception $verifyError) {
                    throw new Exception('La persona fue creada pero no se puede verificar: ' . $verifyError->getMessage());
                }
            }

            if (!$personaExiste) {
                throw new Exception('No se pudo crear o verificar la persona en el sistema de Dominicana');
            }
            
            // Preparar datos para la venta
            // Nota: casaConductor y asistenciaVial van dentro de datosVehiculoRequestList (600006/600007)
            $ventaData = [
                'nuDocumento'              => $transaccion['cedula'],
                'cdTipoPlan'               => $this->mapearTipoPlan($transaccion),
                'datosVehiculoRequestList' => $this->prepararDatosVehiculo($transaccion)
            ];

            $this->logger->info('Preparando datos de venta', [
                'transaction_id' => $transaccion['id'],
                'venta_data' => $ventaData,
                'transaccion_completa' => $transaccion
            ], $transaccion['id']);

            // Procesar venta en Dominicana de Seguros
            try {
                $ventaResponse = $this->dominicanaAPI->procesarVenta($ventaData);
                $this->logger->info('[ETAPA: procesarVenta] Respuesta recibida', [
                    'nuCotizacion' => $ventaResponse['nuCotizacion'] ?? 'NO_PRESENTE',
                    'cdEstado'     => $ventaResponse['cdEstado'] ?? 'NO_PRESENTE',
                    'estado'       => $ventaResponse['estado'] ?? 'NO_PRESENTE',
                    'response_completa' => $ventaResponse
                ], $transaccion['id']);
            } catch (Exception $e) {
                $this->_detectarPolizaActiva($e, $transaccion['id']);
                $this->logger->error('[ETAPA: procesarVenta] FALLO', [
                    'error' => $e->getMessage(),
                    'venta_data' => $ventaData
                ], $transaccion['id']);
                throw $e;
            }

            if (!isset($ventaResponse['nuCotizacion'])) {
                $this->logger->error('[ETAPA: procesarVenta] nuCotizacion no presente en respuesta', [
                    'response_completa' => $ventaResponse
                ], $transaccion['id']);
                throw new Exception('Error en la respuesta de procesamiento de venta: nuCotizacion no presente');
            }

            // Actualizar monto_total con la prima real de DCS
            if (isset($ventaResponse['prima']) && $ventaResponse['prima'] > 0) {
                $connUpd = $this->db->connect();
                $connUpd->prepare("UPDATE transacciones SET monto_total = ? WHERE id = ?")
                        ->execute([$ventaResponse['prima'], $transaccion['id']]);
                $this->logger->info('[ETAPA: procesarVenta] monto_total actualizado con prima DCS', [
                    'transaction_id' => $transaccion['id'],
                    'prima'          => $ventaResponse['prima']
                ], $transaccion['id']);
            }
            } // end else (sin cotización previa)

            // Emitir la póliza - según documentación página 26
            // cdTipoPago: "N" = Notificación de pago (emite inmediatamente)
            //             "T" = Pago con tarjeta (genera productCode)
            $emisionData = [
                'nuCotizacion' => $ventaResponse['nuCotizacion'],
                'cdTipoPago' => 'N',
                'cdUsuario' => getConfigValue('dominicana_usuario', 'DISASHOP')
            ];

            $this->logger->info('[ETAPA: emitirVenta] Preparando emisión', [
                'transaction_id'   => $transaccion['id'],
                'emision_data'     => $emisionData,
                'numero_cotizacion' => $ventaResponse['nuCotizacion'],
                'estado_cotizacion' => $ventaResponse['cdEstado'] ?? $ventaResponse['estado'] ?? 'desconocido'
            ], $transaccion['id']);

            try {
                $emisionResponse = $this->dominicanaAPI->emitirVenta($emisionData);
                $this->logger->info('[ETAPA: emitirVenta] Respuesta recibida', [
                    'response_completa' => $emisionResponse
                ], $transaccion['id']);
            } catch (Exception $e) {
                $this->logger->error('[ETAPA: emitirVenta] FALLO', [
                    'error'            => $e->getMessage(),
                    'emision_data'     => $emisionData,
                    'nuCotizacion'     => $ventaResponse['nuCotizacion'],
                    'estado_cotizacion' => $ventaResponse['cdEstado'] ?? $ventaResponse['estado'] ?? 'desconocido',
                    'venta_response'   => $ventaResponse
                ], $transaccion['id']);
                throw $e;
            }

            // Validar respuesta
            // Si cdTipoPago = 'E' (Efectivo), retorna nuPoliza
            // Si cdTipoPago = 'T' (Tarjeta), retorna productCode
            if (!isset($emisionResponse['nuPoliza']) && !isset($emisionResponse['productCode'])) {
                throw new Exception('Error en la emisión de la póliza: respuesta inválida');
            }

            // Si retorna productCode, significa que requiere pago con tarjeta
            if (isset($emisionResponse['productCode']) && !isset($emisionResponse['nuPoliza'])) {
                throw new Exception('La emisión requiere pago con tarjeta (productCode: ' . $emisionResponse['productCode'] . ')');
            }

            // Actualizar transacción con los datos de la póliza
            $conn = $this->db->connect();
            $stmt = $conn->prepare("
                UPDATE transacciones
                SET numero_cotizacion = ?,
                    numero_poliza = ?,
                    fecha_emision = NOW(),
                    fecha_vencimiento = DATE_ADD(NOW(), INTERVAL 1 YEAR),
                    estado = 'poliza_emitida',
                    updated_at = NOW()
                WHERE id = ?
            ");

            $updateSuccess = $stmt->execute([
                $ventaResponse['nuCotizacion'],
                $emisionResponse['nuPoliza'],
                $transactionId
            ]);

            if (!$updateSuccess) {
                $errorInfo = $stmt->errorInfo();
                $this->logger->error('Error al actualizar estado de transacción', [
                    'transaction_id' => $transactionId,
                    'error_info' => $errorInfo,
                    'numero_poliza' => $emisionResponse['nuPoliza'],
                    'numero_cotizacion' => $ventaResponse['nuCotizacion']
                ], $transactionId);
                throw new Exception('Error al actualizar el estado de la transacción: ' . $errorInfo[2]);
            }

            $rowsAffected = $stmt->rowCount();
            if ($rowsAffected === 0) {
                $this->logger->warning('UPDATE no afectó ninguna fila', [
                    'transaction_id' => $transactionId,
                    'numero_poliza' => $emisionResponse['nuPoliza'],
                    'numero_cotizacion' => $ventaResponse['nuCotizacion']
                ], $transactionId);
            }

            $this->logger->info('Póliza emitida exitosamente', [
                'transaction_id' => $transactionId,
                'numero_poliza' => $emisionResponse['nuPoliza'],
                'numero_cotizacion' => $ventaResponse['nuCotizacion'],
                'rows_affected' => $rowsAffected
            ], $transactionId);

            $this->descargarYGuardarDocumentos($transactionId, $emisionResponse['nuPoliza'], $transaccion);

            // Enviar SMS con link de descarga (URL corta y segura)
            try {
                $downloadLink = $this->urlShortener->generarURLCortaAlternativa($transactionId);

                $this->logger->info('Enviando SMS con link de descarga', [
                    'transaction_id' => $transactionId,
                    'telefono' => $transaccion['telefono'],
                    'link' => $downloadLink
                ], $transactionId);

                $this->smsService->enviarLinkPoliza($transaccion['telefono'], $downloadLink);
            } catch (Exception $e) {
                $this->logger->warning('Error al enviar SMS con link de póliza (continuando)', [
                    'error' => $e->getMessage(),
                    'transaction_id' => $transactionId
                ], $transactionId);
            }

            return [
                'numero_poliza' => $emisionResponse['nuPoliza'],
                'numero_cotizacion' => $ventaResponse['nuCotizacion'],
                'prima' => $ventaResponse['prima'] ?? null,
                'nombre_titular' => $transaccion['nombre_completo']
            ];
        } catch (Exception $e) {
            // Determinar el tipo de error
            $errorMsg = $e->getMessage();
            $estadoError = 'error';

            // Si es un error de vehículo no encontrado, usar estado específico
            if (strpos($errorMsg, 'no encontrado en el catálogo') !== false ||
                strpos($errorMsg, 'No se encontró') !== false) {
                $estadoError = 'vehiculo_no_soportado';
            }

            // Determinar etapa donde falló según el mensaje de error
            $etapaFallo = 'desconocida';
            if (strpos($errorMsg, 'ETAPA: procesarVenta') !== false || strpos($errorMsg, 'nuCotizacion') !== false) {
                $etapaFallo = 'procesarVenta';
            } elseif (strpos($errorMsg, 'ETAPA: emitirVenta') !== false || strpos($errorMsg, 'cotización no válido') !== false || strpos($errorMsg, '422') !== false) {
                $etapaFallo = 'emitirVenta';
            } elseif (strpos($errorMsg, 'persona') !== false || strpos($errorMsg, 'Persona') !== false) {
                $etapaFallo = 'crear_persona';
            } elseif (strpos($errorMsg, 'vehículo') !== false || strpos($errorMsg, 'Vehículo') !== false) {
                $etapaFallo = 'mapear_vehiculo';
            }

            // Actualizar estado a error
            $conn = $this->db->connect();
            $stmt = $conn->prepare("UPDATE transacciones SET estado = ? WHERE id = ?");
            $stmt->execute([$estadoError, $transactionId]);

            $this->logger->error('[ETAPA: ' . $etapaFallo . '] Error emitiendo póliza', [
                'error' => $errorMsg,
                'transaction_id' => $transactionId,
                'etapa_fallo' => $etapaFallo,
                'estado' => $estadoError
            ], $transactionId);

            // Lanzar excepción con mensaje mejorado si es vehículo no soportado
            if ($estadoError === 'vehiculo_no_soportado') {
                throw new Exception(
                    "VEHÍCULO NO SOPORTADO: Este vehículo no está disponible en el catálogo de la aseguradora. " .
                    "Su pago ha sido registrado. Por favor, contacte a soporte para resolver esta situación. " .
                    "ID de transacción: {$transactionId}"
                );
            }

            throw $e;
        }
    }
    
    // Métodos auxiliares
    private function generateUUID() {
        return sprintf(
            '%04x%04x-%04x-%04x-%04x-%04x%04x%04x',
            mt_rand(0, 0xffff), mt_rand(0, 0xffff),
            mt_rand(0, 0xffff),
            mt_rand(0, 0x0fff) | 0x4000,
            mt_rand(0, 0x3fff) | 0x8000,
            mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff)
        );
    }
    
    private function getPlanInfo($cdTipoPlan) {
        $conn = $this->db->connect();
        $stmt = $conn->prepare("SELECT BIN_TO_UUID(id, 1) as id, cd_tipo_plan, descripcion, tipo_vehiculo, vigencia_meses, tipo_cobertura, precio, visible, orden, created_at, updated_at FROM planes WHERE cd_tipo_plan = ? LIMIT 1");
        $stmt->execute([$cdTipoPlan]);
        return $stmt->fetch(PDO::FETCH_ASSOC);
    }
    
    
    private function validarCedula($cedula) {
        $conn = $this->masteroneDb->connect();
        $stmt = $conn->prepare("SELECT * FROM individuos WHERE documento = ? LIMIT 1");
        $stmt->execute([$cedula]);
        return $stmt->fetch();
    }
    
    private function validarPlaca($placa) {
        $conn = $this->masteroneDb->connect();
        $stmt = $conn->prepare("SELECT * FROM vehiculos WHERE placa = ? LIMIT 1");
        $stmt->execute([$placa]);
        return $stmt->fetch();
    }
    
    private function ocultarNombre($nombreCompleto) {
        $nombres = explode(' ', $nombreCompleto);
        $resultado = [];
        foreach ($nombres as $nombre) {
            if (strlen($nombre) > 2) {
                $resultado[] = substr($nombre, 0, 2) . str_repeat('*', strlen($nombre) - 2);
            } else {
                $resultado[] = $nombre;
            }
        }
        return implode(' ', $resultado);
    }
    
    private function ocultarVehiculo($marca, $modelo) {
        $marcaOculta = strlen($marca) > 3 ? substr($marca, 0, 3) . '**' : $marca;
        $modeloOculto = strlen($modelo) > 2 ? substr($modelo, 0, 2) . '**' : $modelo;
        return "$marcaOculta $modeloOculto";
    }
    
    private function generarCodigoOTP($length = OTP_LENGTH) {
        return str_pad(mt_rand(0, pow(10, $length) - 1), $length, '0', STR_PAD_LEFT);
    }
    
    private function enviarSMS($telefono, $mensaje) {
        try {
            // Implementación real con BroadcasterMobile
            $this->logger->info('Enviando SMS real', [
                'telefono' => $telefono,
                'mensaje' => substr($mensaje, 0, 50) . '...'
            ]);

            // Por ahora retornar true (puedes activar el envío real cuando estés listo)
            // Descomentar la siguiente línea para enviar SMS reales:
            // return $this->smsService->enviarOTP($telefono, $mensaje);

            return true;
        } catch (Exception $e) {
            $this->logger->error('Error enviando SMS', [
                'telefono' => $telefono,
                'error' => $e->getMessage()
            ]);
            // No fallar la transacción si el SMS falla
            return false;
        }
    }
    
    private function getTransaccion($id) {
        $conn = $this->db->connect();
        $stmt = $conn->prepare("SELECT * FROM transacciones WHERE id = ?");
        $stmt->execute([$id]);
        return $stmt->fetch();
    }
    
    private function getTransaccionCompleta($id) {
        $conn = $this->db->connect();
        // Obtener datos del plan desde la tabla planes (fuente de verdad)
        $stmt = $conn->prepare("
            SELECT t.*, pl.tipo_vehiculo, pl.tipo_cobertura
            FROM transacciones t
            LEFT JOIN planes pl ON t.cd_tipo_plan = pl.cd_tipo_plan
            WHERE t.id = ?
        ");
        $stmt->execute([$id]);
        return $stmt->fetch();
    }
    
    private function mapearTipoPlan($transaccion) {
        // Usar el código de plan almacenado directamente (tal como lo seleccionó el usuario)
        // El catálogo DCS actual tiene: 1-4 = motocicleta, 5-6 = automóvil
        if (isset($transaccion['cd_tipo_plan']) && !empty($transaccion['cd_tipo_plan'])) {
            $this->logger->info('Usando cd_tipo_plan almacenado', [
                'cd_tipo_plan' => $transaccion['cd_tipo_plan'],
                'tipo_vehiculo' => $transaccion['tipo_vehiculo'] ?? 'desconocido'
            ]);
            return (int)$transaccion['cd_tipo_plan'];
        }

        // Fallback para transacciones antiguas sin cd_tipo_plan: buscar en planes por tipo_vehiculo
        $conn = $this->db->connect();
        $tipoVehiculo = $transaccion['tipo_vehiculo'] ?? 'automovil';
        $stmt = $conn->prepare("SELECT cd_tipo_plan FROM planes WHERE tipo_vehiculo = ? AND visible = 1 ORDER BY vigencia_meses DESC, cd_tipo_plan ASC LIMIT 1");
        $stmt->execute([$tipoVehiculo]);
        $plan = $stmt->fetch(PDO::FETCH_ASSOC);

        if ($plan) {
            $this->logger->info('Plan mapeado desde DB por tipo_vehiculo (fallback)', [
                'tipo_vehiculo' => $tipoVehiculo,
                'cd_tipo_plan'  => $plan['cd_tipo_plan']
            ]);
            return (int)$plan['cd_tipo_plan'];
        }

        // Último fallback hardcoded
        $cdTipoPlan = ($tipoVehiculo === 'motocicleta') ? 1 : 6;
        $this->logger->info('Plan mapeado hardcoded (último fallback)', [
            'tipo_vehiculo' => $tipoVehiculo,
            'cd_tipo_plan'  => $cdTipoPlan
        ]);
        return $cdTipoPlan;
    }

    
    private function prepararDatosVehiculo($transaccion) {
        // Año del vehículo
        $ano = $transaccion['año'] ?? '2020';
        // Mapear color del vehículo
        $colorVehiculo = $transaccion['color'] ?? null;
        $colorId = ColorMapper::mapColor($colorVehiculo);

        // Uso del vehículo según el plan seleccionado (cdTabla 600022)
        $usoDcs = '1'; // default: Privado
        if (!empty($transaccion['cd_tipo_plan'])) {
            $conn = $this->db->connect();
            $stmtUso = $conn->prepare('SELECT uso_dcs FROM planes WHERE cd_tipo_plan = ?');
            $stmtUso->execute([$transaccion['cd_tipo_plan']]);
            $planUso = $stmtUso->fetch(PDO::FETCH_ASSOC);
            if ($planUso && isset($planUso['uso_dcs'])) {
                $usoDcs = (string)$planUso['uso_dcs'];
            }
        }

        // ── Flujo nuevo: usar versión confirmada por el usuario ─────────────────
        if (!empty($transaccion['de_indice_dato_confirmado'])) {
            $deIndiceDato = $transaccion['de_indice_dato_confirmado'];
            $parts = explode('|', $deIndiceDato);
            $cdMarca = $parts[0] ?? '';
            $cdModelo = $parts[1] ?? '';

            $this->logger->info('Usando versión confirmada por el usuario', [
                'de_indice_dato' => $deIndiceDato,
                'cdMarca'        => $cdMarca,
                'cdModelo'       => $cdModelo
            ], $transaccion['id'] ?? null);

            $this->logger->info("Color mapeado: '{$colorVehiculo}' -> ID {$colorId} (" . ColorMapper::getColorName($colorId) . ")");

            return [
                ['cdTabla' => 600006, 'deIndiceDato' => ($transaccion['incluye_grua']            == 1) ? '3' : '0'], // Domiasistencia
                ['cdTabla' => 600007, 'deIndiceDato' => ($transaccion['incluye_casa_conductor']   == 1) ? '1' : '0'], // Casa del Conductor
                ['cdTabla' => 600010, 'deIndiceDato' => $cdMarca],
                ['cdTabla' => 600011, 'deIndiceDato' => $cdMarca . '|' . $cdModelo],
                ['cdTabla' => 600013, 'deIndiceDato' => $deIndiceDato],
                ['cdTabla' => 600021, 'deIndiceDato' => (string)$ano],
                ['cdTabla' => 600022, 'deIndiceDato' => $usoDcs], // Uso según plan seleccionado
                ['cdTabla' => 600023, 'deIndiceDato' => (string)$colorId],
                ['cdTabla' => 600024, 'deIndiceDato' => strtoupper((string)$transaccion['placa'])],
                ['cdTabla' => 600025, 'deIndiceDato' => strtoupper((string)($transaccion['chasis'] ?? 'DEMO123456789'))]
            ];
        }

        // ── Flujo legado: usar VehicleMapper para auto-seleccionar versión ──────
        require_once __DIR__ . '/VehicleMapper.php';
        $conn   = $this->db->connect();
        $mapper = new VehicleMapper($this->dominicanaAPI, $this->logger, $conn);

        try {
            // Mapear el vehículo de MasterOne a Dominicana API
            $vehicleMapped = $mapper->mapVehicle($transaccion['marca'], $transaccion['modelo']);

            $this->logger->info('Vehículo mapeado exitosamente (flujo legado VehicleMapper)', [
                'input'  => ['marca' => $transaccion['marca'], 'modelo' => $transaccion['modelo']],
                'output' => $vehicleMapped
            ], $transaccion['id'] ?? null);

            $this->logger->info("Color mapeado: '{$colorVehiculo}' -> ID {$colorId} (" . ColorMapper::getColorName($colorId) . ")");

            // Construir array de datos del vehículo según documentación actualizada
            $datosVehiculo = [
                ['cdTabla' => 600006, 'deIndiceDato' => ($transaccion['incluye_grua']            == 1) ? '3' : '0'], // Domiasistencia
                ['cdTabla' => 600007, 'deIndiceDato' => ($transaccion['incluye_casa_conductor']   == 1) ? '1' : '0'], // Casa del Conductor
                ['cdTabla' => 600010, 'deIndiceDato' => (string)$vehicleMapped['cdMarca']], // Marca
                ['cdTabla' => 600011, 'deIndiceDato' => $vehicleMapped['cdMarca'] . '|' . $vehicleMapped['cdModelo']], // Modelo
                ['cdTabla' => 600013, 'deIndiceDato' => $vehicleMapped['deIndiceDato']], // Versión (completo: marca|modelo|version)
                ['cdTabla' => 600021, 'deIndiceDato' => (string)$ano], // Año
                ['cdTabla' => 600022, 'deIndiceDato' => $usoDcs], // Uso según plan seleccionado
                ['cdTabla' => 600023, 'deIndiceDato' => (string)$colorId], // Color (mapeado desde ColorMapper)
                ['cdTabla' => 600024, 'deIndiceDato' => strtoupper((string)$transaccion['placa'])], // Placa
                ['cdTabla' => 600025, 'deIndiceDato' => strtoupper((string)($transaccion['chasis'] ?? 'DEMO123456789'))] // Chasis
            ];

            return $datosVehiculo;

        } catch (Exception $e) {
            // NO USAR FALLBACK - Rechazar la cotización si no se encuentra el vehículo
            $this->logger->error('VehicleMapper falló - Vehículo no encontrado en catálogo', [
                'error' => $e->getMessage(),
                'marca' => $transaccion['marca'],
                'modelo' => $transaccion['modelo']
            ], $transaccion['id'] ?? null);

            // Lanzar excepción para rechazar la cotización
            throw new Exception(
                "Vehículo no encontrado en el catálogo de Dominicana Seguros. " .
                "Marca: '{$transaccion['marca']}', Modelo: '{$transaccion['modelo']}'. " .
                "No podemos procesar esta solicitud porque no existe una coincidencia exacta en el catálogo de la aseguradora."
            );
        }
    }

    /**
     * @deprecated Este método NO debe usarse más - Usa códigos genéricos incorrectos
     * Se mantiene solo para referencia histórica
     */
    private function prepararDatosVehiculoLegacy($transaccion) {
        // DEPRECATED: Este método usa códigos genéricos que generan pólizas con información incorrecta
        throw new Exception('Método prepararDatosVehiculoLegacy está deprecated y no debe usarse');

        $marcaCodigo = $this->buscarCodigoMarca($transaccion['marca']);
        $modeloCodigo = $this->buscarCodigoModelo($marcaCodigo, $transaccion['modelo']);
        $ano = $transaccion['año'] ?? '2020';

        $datosVehiculo = [
            ['cdTabla' => 600010, 'deIndiceDato' => (string)$marcaCodigo],
            ['cdTabla' => 600011, 'deIndiceDato' => $marcaCodigo . '|' . $modeloCodigo],
            ['cdTabla' => 600013, 'deIndiceDato' => $marcaCodigo . '|' . $modeloCodigo . '|1'],
            ['cdTabla' => 600021, 'deIndiceDato' => (string)$ano],
            ['cdTabla' => 600022, 'deIndiceDato' => '1'],
            ['cdTabla' => 600023, 'deIndiceDato' => '1'],
            ['cdTabla' => 600024, 'deIndiceDato' => strtoupper((string)$transaccion['placa'])],
            ['cdTabla' => 600025, 'deIndiceDato' => strtoupper((string)($transaccion['chasis'] ?? 'DEMO123456789'))]
        ];

        return $datosVehiculo;
    }

    private function buscarCodigoMarca($nombreMarca) {
        try {
            $marcas = $this->dominicanaAPI->getMarcas();
            $nombreMarcaNormalizado = strtoupper(trim($nombreMarca));

            foreach ($marcas as $marca) {
                $marcaNormalizadaCatalogo = strtoupper(trim($marca['deValorDato']));

                // Búsqueda exacta
                if ($marcaNormalizadaCatalogo === $nombreMarcaNormalizado) {
                    $this->logger->info('Marca encontrada (exacta)', [
                        'marca_buscada' => $nombreMarca,
                        'marca_encontrada' => $marca['deValorDato'],
                        'codigo' => $marca['deIndiceDato']
                    ]);
                    return $marca['deIndiceDato'];
                }

                // Búsqueda parcial (si la marca del catálogo contiene la buscada o viceversa)
                if (strpos($marcaNormalizadaCatalogo, $nombreMarcaNormalizado) !== false ||
                    strpos($nombreMarcaNormalizado, $marcaNormalizadaCatalogo) !== false) {
                    $this->logger->info('Marca encontrada (parcial)', [
                        'marca_buscada' => $nombreMarca,
                        'marca_encontrada' => $marca['deValorDato'],
                        'codigo' => $marca['deIndiceDato']
                    ]);
                    return $marca['deIndiceDato'];
                }
            }

            // Si no se encuentra, usar código genérico
            $this->logger->warning('Marca no encontrada, usando código genérico', [
                'marca_buscada' => $nombreMarca
            ]);
            return '1'; // Código genérico
        } catch (Exception $e) {
            $this->logger->error('Error buscando código de marca', [
                'error' => $e->getMessage(),
                'marca' => $nombreMarca
            ]);
            return '1'; // Fallback a código genérico
        }
    }

    /**
     * Ejecuta la renovación de una póliza a través de DCS /prerenovacion/renovar.
     * Se llama desde emitirPoliza() cuando es_renovacion = 1.
     */
    /**
     * Detecta si el error de DCS indica que el vehículo ya tiene una póliza activa
     * y lanza una excepción con prefijo POLIZA_ACTIVA para que el cliente la maneje apropiadamente.
     */
    private function _detectarPolizaActiva(Exception $e, $transactionId) {
        $msg = $e->getMessage();
        if (stripos($msg, 'ya está registrado en la póliza activa') !== false ||
            stripos($msg, 'ya esta registrado en la poliza activa') !== false ||
            (stripos($msg, 'chasis') !== false && stripos($msg, 'póliza activa') !== false)) {

            // Extraer número de póliza del mensaje (formato: 1-600-XXXXX-1)
            preg_match('/(\d+-\d+-\d+-\d+)/', $msg, $matches);
            $nuPoliza = $matches[1] ?? null;

            // Intentar obtener fecha de vencimiento consultando /venta/listado en DCS
            $fechaHasta      = null;
            $fechaRenovacion = null;
            try {
                if ($nuPoliza) {
                    $cdUsuario = getConfigValue('dominicana_usuario', 'DISASHOP');
                    $emisiones = $this->dominicanaAPI->consultarEmisiones($cdUsuario, [
                        'nuPoliza' => $nuPoliza,
                        'page'     => 1,
                        'size'     => 1
                    ]);
                    // La respuesta puede ser array paginado (content) o array directo
                    $lista = [];
                    if (isset($emisiones['content']) && is_array($emisiones['content'])) {
                        $lista = $emisiones['content'];
                    } elseif (is_array($emisiones) && isset($emisiones[0])) {
                        $lista = $emisiones;
                    }

                    if (!empty($lista[0]['fechaHasta'])) {
                        $fechaHasta = $lista[0]['fechaHasta']; // formato DD/MM/YYYY
                        // Calcular fecha desde la que se puede renovar (90 días antes del vencimiento)
                        $dtVence = DateTime::createFromFormat('d/m/Y', $fechaHasta);
                        if ($dtVence) {
                            $dtRenovacion = clone $dtVence;
                            $dtRenovacion->modify('-90 days');
                            $fechaRenovacion = $dtRenovacion->format('d/m/Y');
                        }
                    }
                }
            } catch (Exception $eConsulta) {
                $this->logger->warning('No se pudo consultar fecha de vencimiento de póliza activa', [
                    'nu_poliza' => $nuPoliza,
                    'error'     => $eConsulta->getMessage()
                ], $transactionId);
            }

            $this->logger->warning('Vehículo con póliza activa detectado', [
                'transaction_id'   => $transactionId,
                'nu_poliza'        => $nuPoliza,
                'fecha_vence'      => $fechaHasta,
                'fecha_renovacion' => $fechaRenovacion,
                'error_dcs'        => $msg
            ], $transactionId);

            $msgClaro = 'POLIZA_ACTIVA: Este vehículo ya tiene una póliza activa'
                      . ($nuPoliza ? " (N° {$nuPoliza})" : '');

            if ($fechaHasta) {
                $msgClaro .= ". Vence el {$fechaHasta}";
            }
            if ($fechaRenovacion) {
                $msgClaro .= ". Puede renovar a partir del {$fechaRenovacion} (90 días antes del vencimiento)";
            } else {
                $msgClaro .= '. La renovación está disponible 90 días o menos antes del vencimiento';
            }
            $msgClaro .= '.';

            throw new Exception($msgClaro);
        }
    }

    private function _ejecutarRenovacion(array $transaccion) {
        $transactionId   = $transaccion['id'];
        $datosRenovacion = json_decode($transaccion['datos_renovacion'] ?? '{}', true) ?: [];

        $nuPoliza = $datosRenovacion['nuPoliza']  ?? null;
        $nuMes    = $datosRenovacion['nuMes']     ?? null;
        $nuAnnio  = $datosRenovacion['nuAnnio']   ?? null;

        if (!$nuPoliza || !$nuMes || !$nuAnnio) {
            throw new Exception('Datos de renovación incompletos (nuPoliza/nuMes/nuAnnio): ' . json_encode($datosRenovacion));
        }

        $this->logger->info('[ETAPA: renovacion] Llamando DCS /prerenovacion/renovar', [
            'transaction_id' => $transactionId,
            'nuPoliza'       => $nuPoliza,
            'nuMes'          => $nuMes,
            'nuAnnio'        => $nuAnnio
        ], $transactionId);

        $renovResponse = $this->dominicanaAPI->renovarPolizas([
            ['nuPoliza' => $nuPoliza, 'nuMes' => $nuMes, 'nuAnnio' => $nuAnnio]
        ]);

        $this->logger->info('[ETAPA: renovacion] Respuesta DCS', [
            'response' => $renovResponse
        ], $transactionId);

        // DCS retorna mensaje de confirmación; si hay 'error' en respuesta, fallar
        if (isset($renovResponse['error'])) {
            throw new Exception('Error en renovación DCS: ' . $renovResponse['error']);
        }
        // Si es un array de resultados, verificar el primero
        if (is_array($renovResponse) && isset($renovResponse[0]['error'])) {
            throw new Exception('Error en renovación DCS: ' . $renovResponse[0]['error']);
        }

        // Formatear número de póliza (puede venir como entero)
        $nuPolizaFormato = '1-600-' . $nuPoliza . '-1';

        // Actualizar transacción
        $conn = $this->db->connect();
        $conn->prepare("
            UPDATE transacciones
            SET numero_poliza = ?, fecha_emision = NOW(),
                fecha_vencimiento = DATE_ADD(NOW(), INTERVAL 1 YEAR),
                estado = 'poliza_emitida', updated_at = NOW()
            WHERE id = ?
        ")->execute([$nuPolizaFormato, $transactionId]);

        // Descargar documentos (marbete) con el mismo sistema que para pólizas nuevas
        try {
            $this->descargarYGuardarDocumentos($transactionId, $nuPolizaFormato, $transaccion);
        } catch (Exception $eDoc) {
            $this->logger->warning('[ETAPA: renovacion] No se pudieron descargar documentos', [
                'error'          => $eDoc->getMessage(),
                'transaction_id' => $transactionId
            ], $transactionId);
        }

        // Enviar SMS
        try {
            $downloadLink = $this->urlShortener->generarURLCortaAlternativa($transactionId);
            $this->smsService->enviarLinkPoliza($transaccion['telefono'], $downloadLink);
        } catch (Exception $eSms) {
            $this->logger->warning('[ETAPA: renovacion] No se pudo enviar SMS', [
                'error' => $eSms->getMessage()
            ], $transactionId);
        }

        return [
            'numero_poliza'  => $nuPolizaFormato,
            'es_renovacion'  => true,
            'nombre_titular' => $transaccion['nombre_completo']
        ];
    }

    private function buscarCodigoModelo($codigoMarca, $nombreModelo) {
        try {
            $modelos = $this->dominicanaAPI->getModelos($codigoMarca);
            $nombreModeloNormalizado = strtoupper(trim($nombreModelo));

            // Normalizar el modelo quitando guiones y números para búsqueda amplia
            // CLA-200 -> CLA, E350 -> E, etc
            $modeloSimplificado = preg_replace('/[-\s]+\d+/', '', $nombreModeloNormalizado);
            $modeloSimplificado = preg_replace('/\d+/', '', $modeloSimplificado);
            $modeloSimplificado = trim($modeloSimplificado);

            $mejorCoincidencia = null;
            $mejorScore = 0;

            foreach ($modelos as $modelo) {
                $modeloNormalizadoCatalogo = strtoupper(trim($modelo['deValorDato']));

                // El código del modelo en deIndiceDato viene en formato "marca|modelo"
                $partes = explode('|', $modelo['deIndiceDato']);
                $codigoModelo = isset($partes[1]) ? $partes[1] : $modelo['deIndiceDato'];

                // Búsqueda exacta
                if ($modeloNormalizadoCatalogo === $nombreModeloNormalizado) {
                    $this->logger->info('Modelo encontrado (exacto)', [
                        'modelo_buscado' => $nombreModelo,
                        'modelo_encontrado' => $modelo['deValorDato'],
                        'codigo' => $codigoModelo
                    ]);
                    return $codigoModelo;
                }

                // Búsqueda exacta de "CLASE {modelo}" (CLA-200 -> CLASE CLA)
                if (!empty($modeloSimplificado)) {
                    $patronClase = 'CLASE ' . $modeloSimplificado;
                    if ($modeloNormalizadoCatalogo === $patronClase) {
                        $this->logger->info('Modelo encontrado (clase exacta)', [
                            'modelo_buscado' => $nombreModelo,
                            'modelo_encontrado' => $modelo['deValorDato'],
                            'codigo' => $codigoModelo
                        ]);
                        return $codigoModelo;
                    }
                }

                // Búsqueda por palabras clave (para CLA-200 -> CLASE CLA)
                if (!empty($modeloSimplificado)) {
                    // Verificar si el modelo del catálogo contiene exactamente el modelo simplificado
                    // "CLASE CLA" contiene "CLA" como palabra completa
                    $patronPalabraCompleta = '/\b' . preg_quote($modeloSimplificado, '/') . '\b/';
                    if (preg_match($patronPalabraCompleta, $modeloNormalizadoCatalogo)) {
                        // Coincidencia de palabra completa tiene prioridad máxima
                        $score = 0.9 + (strlen($modeloSimplificado) / strlen($modeloNormalizadoCatalogo) * 0.1);
                        if ($score > $mejorScore) {
                            $mejorScore = $score;
                            $mejorCoincidencia = [
                                'codigo' => $codigoModelo,
                                'nombre' => $modelo['deValorDato']
                            ];
                        }
                    } elseif (strpos($modeloNormalizadoCatalogo, $modeloSimplificado) !== false) {
                        // Coincidencia parcial tiene menos prioridad
                        $score = strlen($modeloSimplificado) / strlen($modeloNormalizadoCatalogo);
                        if ($score > $mejorScore) {
                            $mejorScore = $score;
                            $mejorCoincidencia = [
                                'codigo' => $codigoModelo,
                                'nombre' => $modelo['deValorDato']
                            ];
                        }
                    }
                }

                // Búsqueda parcial tradicional
                if (strpos($modeloNormalizadoCatalogo, $nombreModeloNormalizado) !== false ||
                    strpos($nombreModeloNormalizado, $modeloNormalizadoCatalogo) !== false) {
                    $score = 0.5;
                    if ($score > $mejorScore) {
                        $mejorScore = $score;
                        $mejorCoincidencia = [
                            'codigo' => $codigoModelo,
                            'nombre' => $modelo['deValorDato']
                        ];
                    }
                }
            }

            // Si encontramos una buena coincidencia, usarla
            if ($mejorCoincidencia && $mejorScore > 0.3) {
                $this->logger->info('Modelo encontrado (coincidencia)', [
                    'modelo_buscado' => $nombreModelo,
                    'modelo_encontrado' => $mejorCoincidencia['nombre'],
                    'codigo' => $mejorCoincidencia['codigo'],
                    'score' => $mejorScore
                ]);
                return $mejorCoincidencia['codigo'];
            }

            // Si no se encuentra, usar código genérico
            $this->logger->warning('Modelo no encontrado, usando código genérico', [
                'modelo_buscado' => $nombreModelo,
                'codigo_marca' => $codigoMarca
            ]);
            return '1'; // Código genérico
        } catch (Exception $e) {
            $this->logger->error('Error buscando código de modelo', [
                'error' => $e->getMessage(),
                'marca_codigo' => $codigoMarca,
                'modelo' => $nombreModelo
            ]);
            return '1'; // Fallback a código genérico
        }
    }

    /**
     * Obtiene la lista de versiones DCS disponibles para el vehículo de la transacción.
     * Usa VehicleMapper para encontrar cdMarca|cdModelo y luego llama a getVersiones.
     * Marca la versión que el algoritmo sugeriría automáticamente.
     */
    public function obtenerVersionesVehiculo($transactionId) {
        $transaccion = $this->getTransaccion($transactionId);
        if (!$transaccion) {
            throw new Exception('Transacción no encontrada');
        }

        $estadosPermitidos = ['otp_validado', 'version_confirmada', 'datos_validados'];
        if (!in_array($transaccion['estado'], $estadosPermitidos)) {
            throw new Exception('Estado de transacción no válido para obtener versiones. Estado: ' . $transaccion['estado']);
        }

        if (empty($transaccion['marca']) || empty($transaccion['modelo'])) {
            throw new Exception('No hay datos de marca/modelo. Valide los datos primero.');
        }

        // Usar VehicleMapper para obtener los códigos de marca y modelo en DCS
        require_once __DIR__ . '/VehicleMapper.php';
        $conn   = $this->db->connect();
        $mapper = new VehicleMapper($this->dominicanaAPI, $this->logger, $conn);

        try {
            $mapped = $mapper->mapVehicle($transaccion['marca'], $transaccion['modelo']);
        } catch (Exception $e) {
            throw new Exception(
                'No se encontró este vehículo en el catálogo de DCS: ' . $e->getMessage()
            );
        }

        $cdMarca             = $mapped['cdMarca'];
        $cdModelo            = $mapped['cdModelo'];
        $sugerido            = $mapped['deIndiceDato']; // "cdMarca|cdModelo|cdVersion"
        $matchType           = $mapped['matchType'] ?? 'fallback';
        $deIndiceDatoParcial = $cdMarca . '|' . $cdModelo;

        // Obtener todas las versiones disponibles para esta marca+modelo
        $versiones = $this->dominicanaAPI->getVersiones($deIndiceDatoParcial);

        if (!is_array($versiones) || empty($versiones)) {
            throw new Exception('No se encontraron versiones disponibles para este vehículo en DCS');
        }

        // Marcar la versión que el mapper identificó
        foreach ($versiones as &$v) {
            $v['sugerido'] = ($v['deIndiceDato'] === $sugerido);
        }
        unset($v);

        $totalVersiones = count($versiones);

        /**
         * Auto-seleccionar versión si:
         *  a) Solo hay una versión disponible (siempre inequívoco), O
         *  b) El mapper encontró coincidencia de alta confianza (exact o suffix)
         *     con el texto de MasterOne — lo que significa que MasterOne ya
         *     traía info de versión identificable en el campo modelo.
         */
        $altaConfianza  = in_array($matchType, ['exact', 'suffix']);
        $versionUnica   = ($totalVersiones === 1) || $altaConfianza;
        $motivoAutosel  = ($totalVersiones === 1) ? 'version_unica_dcs' : "match_{$matchType}";

        if ($versionUnica) {
            $deIndiceDatoAuto = $sugerido; // versión identificada por el mapper
            // Si solo hay una versión DCS disponible, asegurar que usamos esa
            if ($totalVersiones === 1) {
                $deIndiceDatoAuto = $versiones[0]['deIndiceDato'];
            }

            $conn->prepare("
                UPDATE transacciones
                SET de_indice_dato_confirmado = ?, updated_at = NOW()
                WHERE id = ?
            ")->execute([$deIndiceDatoAuto, $transactionId]);

            $this->logger->info('Versión auto-seleccionada', [
                'transaction_id' => $transactionId,
                'de_indice_dato' => $deIndiceDatoAuto,
                'motivo'         => $motivoAutosel,
                'match_type'     => $matchType,
            ], $transactionId);
        }

        $this->logger->info('Versiones de vehículo obtenidas', [
            'transaction_id'  => $transactionId,
            'marca'           => $transaccion['marca'],
            'modelo'          => $transaccion['modelo'],
            'cd_marca'        => $cdMarca,
            'cd_modelo'       => $cdModelo,
            'sugerido'        => $sugerido,
            'match_type'      => $matchType,
            'total_versiones' => $totalVersiones,
            'version_unica'   => $versionUnica
        ], $transactionId);

        return [
            'marca'           => $transaccion['marca'],
            'modelo'          => $transaccion['modelo'],
            'marca_dcs'       => $mapped['nombreMarca']   ?? $transaccion['marca'],
            'modelo_dcs'      => $mapped['nombreModelo']  ?? $transaccion['modelo'],
            'versiones'       => $versiones,
            'sugerido'        => $sugerido,
            'total_versiones' => $totalVersiones,
            'version_unica'   => $versionUnica   // true = cliente salta pantalla de selección
        ];
    }

    /**
     * Confirma la versión del vehículo seleccionada por el usuario y guarda el plan.
     * Obtiene el detalle del vehículo (cilindraje) desde DCS.
     * Actualiza estado a 'version_confirmada'.
     */
    public function confirmarVersionVehiculo($transactionId, $deIndiceDato, $cdTipoPlan) {
        $transaccion = $this->getTransaccion($transactionId);
        if (!$transaccion) {
            throw new Exception('Transacción no encontrada');
        }

        $estadosPermitidos = ['otp_validado', 'datos_validados', 'version_confirmada'];
        if (!in_array($transaccion['estado'], $estadosPermitidos)) {
            throw new Exception('Estado de transacción no válido. Estado: ' . $transaccion['estado']);
        }

        // Validar que el plan existe
        $plan = $this->getPlanInfo($cdTipoPlan);
        if (!$plan) {
            throw new Exception('Plan de seguro no encontrado: ' . $cdTipoPlan);
        }

        // Obtener detalle del vehículo desde DCS (cilindraje, tipoVehiculo)
        $detalleVehiculo = null;
        try {
            $detalleVehiculo = $this->dominicanaAPI->getDetalleVehiculo($deIndiceDato);
            $this->logger->info('Detalle de vehículo obtenido de DCS', [
                'transaction_id'  => $transactionId,
                'de_indice_dato'  => $deIndiceDato,
                'detalle'         => $detalleVehiculo
            ], $transactionId);
        } catch (Exception $e) {
            // No bloquear el flujo si no se puede obtener el detalle
            $this->logger->warning('No se pudo obtener detalle de vehículo desde DCS', [
                'transaction_id' => $transactionId,
                'de_indice_dato' => $deIndiceDato,
                'error'          => $e->getMessage()
            ], $transactionId);
            $detalleVehiculo = [
                'cilindraje'   => null,
                'tonelaje'     => null,
                'tipoVehiculo' => null,
                'claseVehiculo' => null
            ];
        }

        // Guardar versión confirmada + plan en la transacción
        $conn = $this->db->connect();
        $conn->prepare("
            UPDATE transacciones
            SET de_indice_dato_confirmado = ?,
                cd_tipo_plan              = ?,
                estado                    = 'version_confirmada',
                updated_at                = NOW()
            WHERE id = ?
        ")->execute([$deIndiceDato, (int)$cdTipoPlan, $transactionId]);

        $this->logger->info('Versión de vehículo y plan confirmados', [
            'transaction_id'   => $transactionId,
            'de_indice_dato'   => $deIndiceDato,
            'cd_tipo_plan'     => $cdTipoPlan,
            'cilindraje'       => $detalleVehiculo['cilindraje'] ?? 'N/A'
        ], $transactionId);

        return [
            'detalle_vehiculo' => $detalleVehiculo,
            'plan'             => $plan,
            'de_indice_dato'   => $deIndiceDato
        ];
    }

    /**
     * Mapea el tipo de vehículo de MasterOne al tipo local usado en planes.
     * Consulta la tabla tipo_vehiculo_masterone_map. Si el tipo no está en la tabla
     * usa 'automovil' como fallback y lo registra en el log para que el admin lo agregue.
     *
     * @param string|null $tipoMasterone Campo 'tipo' de la tabla vehiculos de MasterOne
     * @return string 'automovil' | 'jeepeta' | 'motocicleta' | 'carga'
     * @throws Exception Si el vehículo está marcado como no asegurable en la tabla de mapeo
     */
    private function mapearTipoVehiculoMasterone($tipoMasterone) {
        if (empty($tipoMasterone)) {
            return 'automovil';
        }

        $tipoNorm = strtoupper(trim($tipoMasterone));

        // Consultar tabla de mapeo explícita
        try {
            $conn = $this->db->connect();
            $stmt = $conn->prepare("SELECT tipo_local, asegurable FROM tipo_vehiculo_masterone_map WHERE tipo_masterone = ?");
            $stmt->execute([$tipoNorm]);
            $row = $stmt->fetch(PDO::FETCH_ASSOC);

            if ($row) {
                if (!$row['asegurable']) {
                    throw new Exception("Tipo de vehículo no asegurable en este sistema: {$tipoMasterone}");
                }
                return $row['tipo_local'];
            }
        } catch (Exception $e) {
            // Re-lanzar si es error de negocio (no asegurable), no si es error de DB
            if (strpos($e->getMessage(), 'no asegurable') !== false) {
                throw $e;
            }
            $this->logger->warning('Error consultando tabla de mapeo, usando fallback', ['error' => $e->getMessage()]);
        }

        // Tipo desconocido — loguear para que el admin lo agregue a la tabla
        $this->logger->warning('Tipo MasterOne no encontrado en tabla de mapeo — usando automovil como fallback', [
            'tipo_masterone' => $tipoMasterone
        ]);
        return 'automovil';
    }

    private function descargarYGuardarDocumentos($transactionId, $numeroPoliza, $transaccion) {
        try {
            $this->logger->info('Iniciando descarga de documentos', [
                'transaction_id' => $transactionId,
                'numero_poliza' => $numeroPoliza
            ], $transactionId);

            $nuCertificado = '1';
            $nuEndoso = '0';

            try {
                $pdfMarbete = $this->dominicanaAPI->descargarPoliza($numeroPoliza, $nuCertificado, $nuEndoso);

                $this->logger->info('Documentos descargados desde API', [
                    'transaction_id' => $transactionId,
                    'numero_poliza' => $numeroPoliza,
                    'tamaño_marbete' => strlen($pdfMarbete)
                ], $transactionId);

                $resultado = $this->documentManager->guardarDocumentos(
                    $transactionId,
                    $pdfMarbete,
                    $pdfMarbete,
                    $numeroPoliza
                );

                $this->logger->info('Documentos guardados exitosamente', [
                    'transaction_id' => $transactionId,
                    'rutas' => $resultado
                ], $transactionId);

                return $resultado;

            } catch (Exception $e) {
                $this->logger->error('Error descargando documentos desde API', [
                    'transaction_id' => $transactionId,
                    'error' => $e->getMessage(),
                    'trace' => $e->getTraceAsString()
                ], $transactionId);
            }

        } catch (Exception $e) {
            $this->logger->error('Error en proceso de descarga y guardado de documentos', [
                'transaction_id' => $transactionId,
                'error' => $e->getMessage()
            ], $transactionId);
        }
    }
}