<div x-data="queryRunner" class="h-full bg-white rounded-lg shadow flex flex-col">

    <!-- Overlay carga -->
    <div x-show="loading" class="fixed inset-0 bg-gray-800 bg-opacity-70 flex justify-center items-center z-50">
        <img src="../img/loading.gif" class="w-14 h-14" />
    </div>

    <div class="flex h-full" style="min-height:calc(100vh - 80px)">

        <!-- ══════════════════ SIDEBAR IZQUIERDO (árbol) ══════════════════ -->
        <aside class="w-64 shrink-0 border-r flex flex-col bg-gray-50">

            <!-- Selector de conexión -->
            <div class="p-3 border-b">
                <label class="text-xs font-semibold text-gray-500 uppercase tracking-wide block mb-1">Conexión DB</label>
                <select x-model="selectedConxId" @change="onConxChange()"
                    class="w-full border rounded px-2 py-1.5 text-xs">
                    <option value="">— Selecciona —</option>
                    <template x-for="c in conexiones" :key="c.ID">
                        <option :value="c.ID"
                            x-text="(c.servidor?.nombre || c.servidor_id) + ' · ' + (c.tipo_db?.nombre || '') + ' :' + c.puerto">
                        </option>
                    </template>
                </select>
                <!-- Test conexión -->
                <button @click="testConn()" x-show="selectedConxId" :disabled="testLoading"
                    class="mt-1.5 w-full text-xs py-1 rounded border border-gray-300 hover:bg-white transition flex items-center justify-center gap-1">
                    <span x-show="!testLoading">⚡ Probar conexión</span>
                    <span x-show="testLoading">Probando…</span>
                </button>
                <p x-show="testMsg" class="text-xs mt-1 font-medium"
                    :class="testOk ? 'text-green-600' : 'text-red-500'" x-text="testMsg"></p>
            </div>

            <!-- Selector de base de datos -->
            <div class="p-3 border-b" x-show="selectedConxId">
                <label class="text-xs font-semibold text-gray-500 uppercase tracking-wide block mb-1">Base de datos</label>
                <select x-model="selectedDb" @change="loadTables()"
                    class="w-full border rounded px-2 py-1.5 text-xs">
                    <option value="">— Selecciona DB —</option>
                    <template x-for="db in databases" :key="db">
                        <option :value="db" x-text="db"></option>
                    </template>
                </select>
            </div>

            <!-- Árbol de tablas / colecciones + visor de campos -->
            <div class="flex-1 flex flex-col overflow-hidden" x-show="tables.length > 0">
                <div class="flex-1 overflow-y-auto p-2">
                    <p class="text-xs font-semibold text-gray-400 uppercase tracking-wide px-1 mb-1" x-text="isMongo ? 'Colecciones' : isRedis ? 'Keys' : 'Tablas'"></p>
                    <template x-for="t in tables" :key="t">
                        <div class="flex items-center group mb-0.5">
                            <button @click="insertTable(t)"
                                class="flex-1 text-left text-xs px-2 py-1 rounded-l hover:bg-[#e9f0cf] text-gray-700 truncate flex items-center gap-1">
                                <svg class="w-3 h-3 shrink-0 text-[#8eb02f]" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
                                    <rect x="3" y="3" width="18" height="18" rx="2" ry="2"/>
                                    <line x1="3" y1="9" x2="21" y2="9"/><line x1="3" y1="15" x2="21" y2="15"/>
                                    <line x1="9" y1="3" x2="9" y2="21"/><line x1="15" y1="3" x2="15" y2="21"/>
                                </svg>
                                <span x-text="t"></span>
                            </button>
                            <!-- Botón ver campos (solo MongoDB) -->
                            <button x-show="isMongo" @click.stop="loadCollectionFields(t)"
                                class="shrink-0 px-1.5 py-1 rounded-r hover:bg-[#d4e89c] text-gray-400 transition"
                                :class="selectedCollection === t ? 'bg-[#d4e89c] !text-[#5a7a1e] opacity-100' : 'opacity-0 group-hover:opacity-100'"
                                title="Ver campos de la colección">
                                <svg class="w-3 h-3" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
                                    <path stroke-linecap="round" stroke-linejoin="round" d="M4 6h16M4 10h16M4 14h16M4 18h7"/>
                                </svg>
                            </button>
                        </div>
                    </template>
                </div>

                <!-- Visor de campos de colección (MongoDB) -->
                <div x-show="isMongo && selectedCollection" class="border-t shrink-0 bg-white flex flex-col" style="max-height:220px">
                    <div class="flex items-center justify-between px-2 py-1.5 bg-[#e9f0cf] border-b shrink-0">
                        <div class="flex items-center gap-1.5 min-w-0">
                            <svg class="w-3 h-3 shrink-0 text-[#5a7a1e]" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
                                <path stroke-linecap="round" stroke-linejoin="round" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
                            </svg>
                            <span class="text-[10px] font-semibold text-[#4a6e18] font-mono truncate" x-text="selectedCollection"></span>
                        </div>
                        <div class="flex items-center gap-1 shrink-0 ml-1">
                            <span x-show="loadingFields" class="text-[10px] text-gray-500">↻</span>
                            <span x-show="!loadingFields && collectionFields.length" class="text-[10px] text-gray-500" x-text="collectionFields.length + ' campos'"></span>
                            <button @click="selectedCollection=''; collectionFields=[]" class="text-gray-400 hover:text-gray-700 ml-1 text-xs leading-none" title="Cerrar">✕</button>
                        </div>
                    </div>
                    <div class="overflow-y-auto flex-1">
                        <div x-show="loadingFields && !collectionFields.length" class="px-3 py-2 text-xs text-gray-400">Cargando campos…</div>
                        <div x-show="!loadingFields && !collectionFields.length && selectedCollection" class="px-3 py-2 text-xs text-gray-400">Sin documentos en esta colección.</div>
                        <template x-for="f in collectionFields" :key="f.name">
                            <div class="flex items-center gap-1 px-2 py-1.5 hover:bg-[#f0f7d8] group border-b border-gray-50">
                                <div class="flex-1 min-w-0">
                                    <div class="font-mono text-xs text-gray-800 truncate" x-text="f.name"></div>
                                    <div class="font-mono text-[9px] text-gray-400 truncate" x-text="f.preview"></div>
                                </div>
                                <div class="shrink-0 flex items-center gap-1 opacity-0 group-hover:opacity-100 transition">
                                    <span class="text-[9px] px-1 py-0.5 rounded bg-gray-100 text-gray-500" x-text="f.type"></span>
                                    <button @click="buildUpdateField(f.name)"
                                        class="text-[10px] px-1.5 py-0.5 rounded text-white"
                                        style="background-color:#8eb02f" title="Generar updateOne para este campo">✎</button>
                                </div>
                            </div>
                        </template>
                    </div>
                </div>
            </div>

            <!-- Mensaje sin conexión -->
            <div x-show="!selectedConxId" class="flex-1 flex items-center justify-center p-4">
                <p class="text-xs text-gray-400 text-center">Selecciona una conexión para explorar bases de datos y tablas.</p>
            </div>
        </aside>

        <!-- ══════════════════ ÁREA PRINCIPAL ══════════════════ -->
        <div class="flex-1 flex flex-col min-w-0">

            <!-- Barra de herramientas -->
            <div class="flex items-center gap-2 px-4 py-2.5 border-b bg-white">
                <button @click="runQuery()"
                    :disabled="!selectedConxId || loadingQuery"
                    class="flex items-center gap-1.5 px-4 py-1.5 text-white text-xs font-semibold rounded-lg transition disabled:opacity-40"
                    style="background-color:#8eb02f"
                    onmouseover="this.style.backgroundColor='#6d8c24'"
                    onmouseout="this.style.backgroundColor='#8eb02f'">
                    <svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" stroke-width="2.5" viewBox="0 0 24 24">
                        <polygon points="5 3 19 12 5 21 5 3"/>
                    </svg>
                    <span x-text="loadingQuery ? 'Ejecutando…' : 'Ejecutar'"></span>
                    <kbd class="ml-1 text-[10px] opacity-70">Ctrl+↵</kbd>
                </button>

                <button @click="clearEditor()" class="px-3 py-1.5 text-xs border rounded hover:bg-gray-50 transition">
                    Limpiar
                </button>

                <!-- Batch mode toggle -->
                <button @click="batchMode = !batchMode; if(!batchMode) batchResults = []"
                    class="px-3 py-1.5 text-xs border rounded transition"
                    :class="batchMode ? 'bg-amber-500 text-white border-amber-500' : 'hover:bg-gray-50'">
                    <span x-text="batchMode ? '📚 Lote ON' : '📄 Lote OFF'"></span>
                </button>

                <!-- Subir .sql -->
                <label x-show="!isMongo && !isRedis" class="px-3 py-1.5 text-xs border rounded hover:bg-gray-50 cursor-pointer transition">
                    📁 Subir .sql
                    <input type="file" accept=".sql" @change="uploadSQLFile($event)" class="hidden">
                </label>

                <!-- Botón AI -->
                <button @click="askAI()" :disabled="aiLoading || !sqlText.trim()"
                    class="flex items-center gap-1 px-3 py-1.5 text-xs rounded transition disabled:opacity-40"
                    :class="aiLoading ? 'bg-purple-100 text-purple-700 border border-purple-200' : 'bg-purple-50 text-purple-700 border border-purple-200 hover:bg-purple-100'">
                    <svg x-show="!aiLoading" class="w-3.5 h-3.5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
                        <path d="M12 3c.132 0 .263 0 .393 0a7.5 7.5 0 007.92 12.446A9 9 0 1112 2.992z"/>
                        <path d="M17 4a2 2 0 012 2M19 4a2 2 0 012 2M19 8a2 2 0 01-2 2M17 8a2 2 0 01-2-2"/>
                    </svg>
                    <svg x-show="aiLoading" class="w-3.5 h-3.5 animate-spin" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
                        <path stroke-linecap="round" stroke-linejoin="round" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/>
                    </svg>
                    <span x-text="aiLoading ? 'Pensando…' : 'AI Sugerir'"></span>
                </button>

                <div class="flex-1"></div>

                <!-- Exportar -->
                <div class="flex items-center gap-1" x-show="results.length > 0">
                    <span class="text-xs text-gray-400" x-text="results.length + ' filas'"></span>
                    <button @click="exportData('csv')"
                        class="px-3 py-1.5 text-xs border border-green-300 text-green-700 rounded hover:bg-green-50 transition">
                        ↓ CSV
                    </button>
                    <button @click="exportData('json')"
                        class="px-3 py-1.5 text-xs border border-blue-300 text-blue-700 rounded hover:bg-blue-50 transition">
                        ↓ JSON
                    </button>
                </div>

                <!-- Tab historial -->
                <button @click="activeTab = activeTab === 'history' ? 'results' : 'history'"
                    class="px-3 py-1.5 text-xs border rounded transition"
                    :class="activeTab === 'history' ? 'bg-[#8eb02f] text-white border-[#8eb02f]' : 'hover:bg-gray-50'">
                    Historial
                </button>
            </div>

            <!-- Editor SQL / MongoDB -->
            <div class="px-4 pt-3 pb-0">
                <div class="border rounded-lg overflow-hidden bg-gray-900">
                    <div class="flex items-center gap-2 px-3 py-1.5 bg-gray-800 border-b border-gray-700">
                        <span class="text-xs text-gray-400" x-text="isMongo ? 'MongoDB Shell' : isRedis ? 'Redis CLI' : 'SQL Editor'"></span>
                        <div class="flex-1"></div>
                        <span x-show="selectedDb" class="text-xs text-[#4ade80] font-mono" x-text="selectedDb"></span>
                    </div>
                    <div class="relative">
                        <textarea id="sql-editor" x-model="sqlText"
                            @input.debounce.150ms="onSqlInput()"
                            @keydown="onSqlKeydown($event)"
                            @keydown.ctrl.enter.prevent="runQuery()"
                            @keydown.meta.enter.prevent="runQuery()"
                            @scroll="closeAutocomplete()"
                            @click="closeAutocomplete()"
                            :placeholder="isMongo ? 'db.coleccion.find({})\n// Ctrl+Enter para ejecutar\n// db.coleccion.insertOne({campo: \'valor\'})' : isRedis ? '# Comandos Redis (uno por línea)\nKEYS *\nGET mi_clave\nHGETALL mi_hash\n# Ctrl+Enter para ejecutar' : '-- Escribe tu consulta SQL aquí\n-- Ctrl+Enter para ejecutar\nSELECT * FROM tabla LIMIT 100;'"
                            class="w-full bg-gray-900 text-gray-100 font-mono text-sm p-4 resize-none outline-none leading-relaxed"
                            style="height:180px; tab-size:2;"></textarea>
                        <!-- Autocomplete dropdown -->
                        <div x-show="autocompleteVisible" x-cloak
                            :style="'position:absolute;left:'+autocompletePos.left+'px;top:'+autocompletePos.top+'px;z-index:100;'"
                            class="bg-white border border-gray-300 rounded-lg shadow-xl max-h-48 overflow-y-auto min-w-[180px]">
                            <template x-for="(item, i) in autocompleteItems" :key="i">
                                <div @mousedown.prevent="selectAutocomplete(i)"
                                    class="px-3 py-1.5 text-xs font-mono cursor-pointer flex items-center gap-2"
                                    :class="i === autocompleteIdx ? 'bg-[#8eb02f] text-white' : 'hover:bg-gray-100 text-gray-700'">
                                    <!-- Icono por tipo -->
                                    <span class="text-[9px] shrink-0 w-12 truncate opacity-60 font-sans"
                                        :class="{
                                            'text-blue-500': item.type === 'keyword',
                                            'text-amber-600': item.type === 'table',
                                            'text-green-600': item.type.startsWith('col')
                                        }"
                                        x-text="item.type === 'keyword' ? 'KW' : item.type === 'table' ? 'tabla' : item.type.replace('col·','')">
                                    </span>
                                    <span class="flex-1 truncate" x-text="item.label"></span>
                                </div>
                            </template>
                        </div>
                        <!-- Typo suggestions -->
                        <div x-show="typoSuggestions.length > 0" x-cloak
                            class="absolute bottom-0 left-0 right-0 px-3 py-1.5 bg-yellow-900 bg-opacity-90 flex flex-wrap gap-1.5 items-center">
                            <span class="text-yellow-300 text-[10px] shrink-0">¿Quisiste decir?</span>
                            <template x-for="(s, i) in typoSuggestions" :key="i">
                                <button @mousedown.prevent="applyTypoFix(s)"
                                    class="text-[10px] px-2 py-0.5 rounded bg-yellow-400 text-yellow-900 hover:bg-yellow-300 transition font-mono font-semibold">
                                    <span x-text="s"></span>
                                </button>
                            </template>
                        </div>
                    </div>
                </div>

                <!-- ═══ BARRA NL2SQL (cool) ═══ -->
                <div x-show="!isMongo && !isRedis && selectedConxId" class="mt-2 rounded-xl overflow-hidden"
                    style="background: linear-gradient(135deg, #1e1b4b 0%, #312e81 40%, #1e3a5f 100%); border: 1px solid #4c1d95">
                    <div class="flex items-center gap-2 px-3 py-2">
                        <!-- Icono mágico animado -->
                        <div class="shrink-0 flex items-center justify-center w-7 h-7 rounded-lg"
                            style="background: linear-gradient(135deg,#7c3aed,#2563eb)"
                            :class="nl2sqlLoading ? 'animate-pulse' : ''">
                            <svg class="w-4 h-4 text-white" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
                                <path stroke-linecap="round" stroke-linejoin="round"
                                    d="M9.813 15.904L9 18.75l-.813-2.846a4.5 4.5 0 00-3.09-3.09L2.25 12l2.846-.813a4.5 4.5 0 003.09-3.09L9 5.25l.813 2.846a4.5 4.5 0 003.09 3.09L15.75 12l-2.846.813a4.5 4.5 0 00-3.09 3.09z"/>
                                <path stroke-linecap="round" stroke-linejoin="round"
                                    d="M18.259 8.715L18 9.75l-.259-1.035a3.375 3.375 0 00-2.455-2.456L14.25 6l1.036-.259a3.375 3.375 0 002.455-2.456L18 2.25l.259 1.035a3.375 3.375 0 002.456 2.456L21.75 6l-1.035.259a3.375 3.375 0 00-2.456 2.456z"/>
                            </svg>
                        </div>
                        <input id="nl2sql-input" x-model="nl2sqlText"
                            @keydown.enter="generateNL2SQL()"
                            :disabled="nl2sqlLoading"
                            type="text"
                            placeholder="Describe en español lo que quieres consultar... ej: trae los usuarios activos creados este mes"
                            class="flex-1 bg-transparent text-white text-xs placeholder-indigo-300 outline-none font-sans">
                        <button @click="generateNL2SQL()" :disabled="nl2sqlLoading || !nl2sqlText.trim()"
                            class="shrink-0 flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-semibold transition disabled:opacity-40"
                            style="background: linear-gradient(135deg,#7c3aed,#2563eb); color:white"
                            onmouseover="this.style.opacity='0.85'"
                            onmouseout="this.style.opacity='1'">
                            <svg x-show="!nl2sqlLoading" class="w-3.5 h-3.5" fill="none" stroke="currentColor" stroke-width="2.5" viewBox="0 0 24 24">
                                <path stroke-linecap="round" stroke-linejoin="round" d="M5 12h14M12 5l7 7-7 7"/>
                            </svg>
                            <svg x-show="nl2sqlLoading" class="w-3.5 h-3.5 animate-spin" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
                                <path stroke-linecap="round" stroke-linejoin="round" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/>
                            </svg>
                            <span x-text="nl2sqlLoading ? 'Generando…' : 'Generar SQL'"></span>
                        </button>
                    </div>
                    <!-- Respuesta inline -->
                    <div x-show="nl2sqlResult" x-cloak class="px-3 pb-2">
                        <div class="rounded-lg overflow-hidden border border-indigo-600">
                            <div class="flex items-center justify-between px-3 py-1.5 bg-indigo-900 bg-opacity-60">
                                <span class="text-[10px] text-indigo-200 font-semibold">SQL generado</span>
                                <div class="flex gap-1.5">
                                    <button @click="applyNL2SQL()"
                                        class="text-[10px] px-2 py-0.5 rounded font-semibold transition"
                                        style="background:#8eb02f;color:white"
                                        onmouseover="this.style.background='#6d8c24'"
                                        onmouseout="this.style.background='#8eb02f'">
                                        Aplicar al editor
                                    </button>
                                    <button @click="nl2sqlResult=''"
                                        class="text-[10px] px-1.5 text-indigo-300 hover:text-white transition">✕</button>
                                </div>
                            </div>
                            <pre class="px-3 py-2 text-xs font-mono text-green-300 bg-gray-950 whitespace-pre-wrap max-h-24 overflow-y-auto" x-text="nl2sqlResult"></pre>
                        </div>
                    </div>
                </div>
                <!-- ─── Helpers SQL (solo MySQL/PostgreSQL, no Redis) ─── -->
                <div x-show="!isMongo && !isRedis && selectedConxId" class="mt-1.5">
                    <!-- Chips rápidos -->
                    <div class="flex flex-wrap gap-1 mb-1.5">
                        <template x-for="h in sqlHelpers" :key="h.label">
                            <button @click="sqlText = h.cmd" class="text-[10px] px-2 py-0.5 rounded bg-gray-100 hover:bg-gray-200 text-gray-600 font-mono transition" x-text="h.label"></button>
                        </template>
                    </div>

                    <!-- ══ Generador DBA ══ -->
                    <div class="border rounded-lg overflow-hidden">
                        <button @click="showDbHelper = !showDbHelper"
                            class="w-full flex items-center gap-1.5 px-3 py-1.5 bg-gray-50 hover:bg-[#e9f0cf] text-xs text-gray-600 transition text-left select-none">
                            <svg class="w-3 h-3 transition-transform duration-150" :class="showDbHelper ? 'rotate-90' : ''"
                                fill="none" stroke="currentColor" stroke-width="2.5" viewBox="0 0 24 24">
                                <path stroke-linecap="round" stroke-linejoin="round" d="M9 5l7 7-7 7"/>
                            </svg>
                            <span class="font-semibold">Generador DBA</span>
                            <span x-show="isPostgres" class="ml-1 text-[10px] px-1.5 py-0.5 rounded bg-blue-100 text-blue-600 font-medium">PostgreSQL</span>
                            <span x-show="!isPostgres && !isRedis" class="ml-1 text-[10px] px-1.5 py-0.5 rounded bg-orange-100 text-orange-600 font-medium">MySQL</span>
                            <span class="ml-auto text-[10px] text-gray-400">CREATE · GRANT · DROP · Setup</span>
                        </button>
                        <div x-show="showDbHelper" x-transition class="p-3 bg-white border-t">
                            <div class="grid grid-cols-2 gap-2 text-xs">
                                <div>
                                    <label class="text-[10px] text-gray-500 font-semibold uppercase block mb-0.5">Nombre BD</label>
                                    <input x-model="dbHelper.db" list="dba-dbs"
                                        class="w-full border rounded px-2 py-1 font-mono focus:outline-none focus:ring-1 focus:ring-[#8eb02f]"
                                        placeholder="mi_base_datos">
                                    <datalist id="dba-dbs">
                                        <template x-for="d in databases" :key="d"><option :value="d"></option></template>
                                    </datalist>
                                </div>
                                <div>
                                    <label class="text-[10px] text-gray-500 font-semibold uppercase block mb-0.5">Operación</label>
                                    <select x-model="dbHelper.op" class="w-full border rounded px-2 py-1 focus:outline-none focus:ring-1 focus:ring-[#8eb02f]">
                                        <option value="full_setup">✦ Setup completo (BD + usuario + permisos)</option>
                                        <option value="create_db">Crear base de datos</option>
                                        <option value="create_user">Crear usuario</option>
                                        <option value="grant">Dar permisos totales (GRANT ALL)</option>
                                        <option value="revoke">Revocar permisos</option>
                                        <option value="show_grants">Ver permisos de usuario</option>
                                        <option value="drop_user">Eliminar usuario</option>
                                        <option value="drop_db">Eliminar base de datos</option>
                                    </select>
                                </div>
                                <div x-show="['full_setup','create_user','grant','revoke','show_grants','drop_user'].includes(dbHelper.op)">
                                    <label class="text-[10px] text-gray-500 font-semibold uppercase block mb-0.5">Usuario</label>
                                    <input x-model="dbHelper.user"
                                        class="w-full border rounded px-2 py-1 font-mono focus:outline-none focus:ring-1 focus:ring-[#8eb02f]"
                                        placeholder="usuario_app">
                                </div>
                                <div x-show="['full_setup','create_user','grant','revoke','show_grants','drop_user'].includes(dbHelper.op) && !isPostgres">
                                    <label class="text-[10px] text-gray-500 font-semibold uppercase block mb-0.5">Host</label>
                                    <input x-model="dbHelper.host"
                                        class="w-full border rounded px-2 py-1 font-mono focus:outline-none focus:ring-1 focus:ring-[#8eb02f]"
                                        placeholder="%">
                                </div>
                                <div x-show="['full_setup','create_user'].includes(dbHelper.op)" class="col-span-2">
                                    <label class="text-[10px] text-gray-500 font-semibold uppercase block mb-0.5">Contraseña</label>
                                    <input x-model="dbHelper.pass" type="text"
                                        class="w-full border rounded px-2 py-1 font-mono focus:outline-none focus:ring-1 focus:ring-[#8eb02f]"
                                        placeholder="Contraseña segura">
                                    <p class="text-[9px] text-gray-400 mt-0.5">Se insertará tal cual en el SQL generado.</p>
                                </div>
                            </div>
                            <div class="flex items-center gap-2 mt-2.5 pt-2.5 border-t">
                                <button @click="generateDbHelper()"
                                    class="flex-1 py-1.5 text-xs text-white font-semibold rounded transition"
                                    style="background-color:#8eb02f"
                                    onmouseover="this.style.backgroundColor='#6d8c24'"
                                    onmouseout="this.style.backgroundColor='#8eb02f'">
                                    ↗ Generar en editor
                                </button>
                            </div>
                        </div>
                    </div>
                </div>

                <!-- Ayuda rápida MongoDB -->
                <div x-show="isMongo" class="mt-1.5">
                    <div class="flex flex-wrap gap-1 mb-1.5">
                        <template x-for="ex in mongoExamples" :key="ex.label">
                            <button @click="sqlText = ex.cmd" class="text-[10px] px-2 py-0.5 rounded bg-gray-100 hover:bg-gray-200 text-gray-600 font-mono transition" x-text="ex.label"></button>
                        </template>
                    </div>

                    <!-- ══ Generador de actualización rápida ══ -->
                    <div class="border rounded-lg overflow-hidden">
                        <button @click="showUpdateBuilder = !showUpdateBuilder"
                            class="w-full flex items-center gap-1.5 px-3 py-1.5 bg-gray-50 hover:bg-[#e9f0cf] text-xs text-gray-600 transition text-left select-none">
                            <svg class="w-3 h-3 transition-transform duration-150" :class="showUpdateBuilder ? 'rotate-90' : ''"
                                fill="none" stroke="currentColor" stroke-width="2.5" viewBox="0 0 24 24">
                                <path stroke-linecap="round" stroke-linejoin="round" d="M9 5l7 7-7 7"/>
                            </svg>
                            <span class="font-semibold">Generador de actualización</span>
                            <span class="ml-auto text-[10px] text-gray-400">updateOne / updateMany</span>
                        </button>
                        <div x-show="showUpdateBuilder" x-transition class="p-3 bg-white border-t">
                            <div class="grid grid-cols-2 gap-2 text-xs">
                                <div>
                                    <label class="text-[10px] text-gray-500 font-semibold uppercase block mb-0.5">Colección</label>
                                    <input x-model="updateBuilder.collection" list="ub-collections"
                                        class="w-full border rounded px-2 py-1 font-mono focus:outline-none focus:ring-1 focus:ring-[#8eb02f]"
                                        :placeholder="selectedCollection || 'coleccion'">
                                    <datalist id="ub-collections">
                                        <template x-for="t in tables" :key="t"><option :value="t"></option></template>
                                    </datalist>
                                </div>
                                <div>
                                    <label class="text-[10px] text-gray-500 font-semibold uppercase block mb-0.5">Operación</label>
                                    <select x-model="updateBuilder.op" class="w-full border rounded px-2 py-1 focus:outline-none focus:ring-1 focus:ring-[#8eb02f]">
                                        <option value="$set">$set — asignar valor</option>
                                        <option value="$unset">$unset — eliminar campo</option>
                                        <option value="$inc">$inc — incrementar número</option>
                                        <option value="$push">$push — añadir a array</option>
                                        <option value="$pull">$pull — quitar de array</option>
                                        <option value="$rename">$rename — renombrar campo</option>
                                    </select>
                                </div>
                                <div>
                                    <label class="text-[10px] text-gray-500 font-semibold uppercase block mb-0.5">Campo a actualizar</label>
                                    <input x-model="updateBuilder.field" list="ub-fields"
                                        class="w-full border rounded px-2 py-1 font-mono focus:outline-none focus:ring-1 focus:ring-[#8eb02f]" placeholder="nombre_campo">
                                    <datalist id="ub-fields">
                                        <template x-for="f in collectionFields" :key="f.name"><option :value="f.name"></option></template>
                                    </datalist>
                                </div>
                                <div>
                                    <label class="text-[10px] text-gray-500 font-semibold uppercase block mb-0.5">
                                        <span x-text="updateBuilder.op === '$rename' ? 'Nuevo nombre' : 'Nuevo valor'"></span>
                                    </label>
                                    <input x-model="updateBuilder.value"
                                        class="w-full border rounded px-2 py-1 font-mono focus:outline-none focus:ring-1 focus:ring-[#8eb02f]"
                                        :placeholder="updateBuilder.op === '$inc' ? '1' : updateBuilder.op === '$rename' ? 'nuevo_nombre' : 'Admin2026!'"
                                        x-show="updateBuilder.op !== '$unset'">
                                    <p x-show="updateBuilder.op !== '$unset'" class="text-[9px] text-gray-400 mt-0.5 leading-tight">
                                        Los strings se auto-encierran en comillas. Para JSON explícito usa <span class="font-mono">"valor"</span>, <span class="font-mono">123</span>, <span class="font-mono">true</span>, <span class="font-mono">{}</span>.
                                    </p>
                                    <span x-show="updateBuilder.op === '$unset'" class="text-[10px] text-gray-400 block py-1.5 italic">El campo será eliminado del documento.</span>
                                </div>
                                <div>
                                    <label class="text-[10px] text-gray-500 font-semibold uppercase block mb-0.5">Filtro — campo</label>
                                    <input x-model="updateBuilder.filterField" list="ub-fields"
                                        class="w-full border rounded px-2 py-1 font-mono focus:outline-none focus:ring-1 focus:ring-[#8eb02f]" placeholder="_id">
                                </div>
                                <div>
                                    <label class="text-[10px] text-gray-500 font-semibold uppercase block mb-0.5">Filtro — valor</label>
                                    <input x-model="updateBuilder.filterValue"
                                        class="w-full border rounded px-2 py-1 font-mono focus:outline-none focus:ring-1 focus:ring-[#8eb02f]" placeholder='"id_del_doc"'>
                                </div>
                            </div>
                            <div class="flex items-center gap-2 mt-2.5 pt-2.5 border-t">
                                <select x-model="updateBuilder.multi" class="border rounded px-2 py-1.5 text-xs focus:outline-none focus:ring-1 focus:ring-[#8eb02f]">
                                    <option value="one">updateOne</option>
                                    <option value="many">updateMany</option>
                                </select>
                                <button @click="generateUpdate()"
                                    class="flex-1 py-1.5 text-xs text-white font-semibold rounded transition"
                                    style="background-color:#8eb02f"
                                    onmouseover="this.style.backgroundColor='#6d8c24'"
                                    onmouseout="this.style.backgroundColor='#8eb02f'">
                                    ↗ Generar en editor
                                </button>
                            </div>
                        </div>
                    </div>
                </div>

                <!-- ─── Helpers Redis ─── -->
                <div x-show="isRedis && selectedConxId" class="mt-1.5">
                    <div class="flex flex-wrap gap-1 mb-1.5">
                        <template x-for="h in redisHelpers" :key="h.label">
                            <button @click="sqlText = h.cmd" class="text-[10px] px-2 py-0.5 rounded bg-red-50 hover:bg-red-100 text-red-700 font-mono transition" x-text="h.label"></button>
                        </template>
                    </div>

                    <!-- ══ Generador Redis ══ -->
                    <div class="border rounded-lg overflow-hidden">
                        <button @click="showRedisBuilder = !showRedisBuilder"
                            class="w-full flex items-center gap-1.5 px-3 py-1.5 bg-gray-50 hover:bg-red-50 text-xs text-gray-600 transition text-left select-none">
                            <svg class="w-3 h-3 transition-transform duration-150" :class="showRedisBuilder ? 'rotate-90' : ''"
                                fill="none" stroke="currentColor" stroke-width="2.5" viewBox="0 0 24 24">
                                <path stroke-linecap="round" stroke-linejoin="round" d="M9 5l7 7-7 7"/>
                            </svg>
                            <span class="font-semibold">Generador Redis</span>
                            <span class="ml-1 text-[10px] px-1.5 py-0.5 rounded bg-red-100 text-red-600 font-medium">Redis</span>
                            <span class="ml-auto text-[10px] text-gray-400">GET · SET · DEL · HGETALL · TTL</span>
                        </button>
                        <div x-show="showRedisBuilder" x-transition class="p-3 bg-white border-t">
                            <div class="grid grid-cols-2 gap-2 text-xs">
                                <div>
                                    <label class="text-[10px] text-gray-500 font-semibold uppercase block mb-0.5">Comando</label>
                                    <select x-model="redisBuilder.cmd" class="w-full border rounded px-2 py-1 focus:outline-none focus:ring-1 focus:ring-red-400">
                                        <option value="GET">GET — obtener valor</option>
                                        <option value="SET">SET — asignar valor</option>
                                        <option value="SETEX">SETEX — asignar con TTL</option>
                                        <option value="DEL">DEL — eliminar clave(s)</option>
                                        <option value="EXISTS">EXISTS — ¿existe la clave?</option>
                                        <option value="TTL">TTL — tiempo de vida (seg)</option>
                                        <option value="EXPIRE">EXPIRE — establecer TTL</option>
                                        <option value="TYPE">TYPE — tipo de la clave</option>
                                        <option value="KEYS">KEYS — buscar por patrón</option>
                                        <option value="SCAN">SCAN — iterar claves</option>
                                        <option value="HSET">HSET — asignar campo hash</option>
                                        <option value="HGET">HGET — obtener campo hash</option>
                                        <option value="HGETALL">HGETALL — todo el hash</option>
                                        <option value="HDEL">HDEL — eliminar campo hash</option>
                                        <option value="LPUSH">LPUSH — añadir a lista</option>
                                        <option value="LRANGE">LRANGE — leer rango de lista</option>
                                        <option value="SADD">SADD — añadir a set</option>
                                        <option value="SMEMBERS">SMEMBERS — todos los miembros</option>
                                        <option value="ZADD">ZADD — añadir a sorted set</option>
                                        <option value="ZRANGE">ZRANGE — rango sorted set</option>
                                        <option value="DBSIZE">DBSIZE — total de claves</option>
                                        <option value="FLUSHDB">FLUSHDB — vaciar base de datos</option>
                                        <option value="INFO">INFO — info del servidor</option>
                                        <option value="CONFIG GET">CONFIG GET — ver config</option>
                                    </select>
                                </div>
                                <div>
                                    <label class="text-[10px] text-gray-500 font-semibold uppercase block mb-0.5">Clave</label>
                                    <input x-model="redisBuilder.key" list="redis-keys"
                                        class="w-full border rounded px-2 py-1 font-mono focus:outline-none focus:ring-1 focus:ring-red-400"
                                        placeholder="mi:clave:aqui">
                                    <datalist id="redis-keys">
                                        <template x-for="k in tables" :key="k"><option :value="k"></option></template>
                                    </datalist>
                                </div>
                                <div x-show="['SET','SETEX','HSET','LPUSH','SADD','ZADD'].includes(redisBuilder.cmd)" class="col-span-2">
                                    <label class="text-[10px] text-gray-500 font-semibold uppercase block mb-0.5">Valor</label>
                                    <input x-model="redisBuilder.value"
                                        class="w-full border rounded px-2 py-1 font-mono focus:outline-none focus:ring-1 focus:ring-red-400"
                                        placeholder="valor">
                                </div>
                                <div x-show="['SETEX','EXPIRE'].includes(redisBuilder.cmd)">
                                    <label class="text-[10px] text-gray-500 font-semibold uppercase block mb-0.5">TTL (segundos)</label>
                                    <input x-model="redisBuilder.ttl" type="number"
                                        class="w-full border rounded px-2 py-1 font-mono focus:outline-none focus:ring-1 focus:ring-red-400"
                                        placeholder="3600">
                                </div>
                                <div x-show="['HSET','HGET','HDEL'].includes(redisBuilder.cmd)">
                                    <label class="text-[10px] text-gray-500 font-semibold uppercase block mb-0.5">Campo (field)</label>
                                    <input x-model="redisBuilder.field"
                                        class="w-full border rounded px-2 py-1 font-mono focus:outline-none focus:ring-1 focus:ring-red-400"
                                        placeholder="campo">
                                </div>
                            </div>
                            <div class="flex items-center gap-2 mt-2.5 pt-2.5 border-t">
                                <button @click="generateRedisCmd()"
                                    class="flex-1 py-1.5 text-xs text-white font-semibold rounded transition"
                                    style="background-color:#dc2626"
                                    onmouseover="this.style.backgroundColor='#b91c1c'"
                                    onmouseout="this.style.backgroundColor='#dc2626'">
                                    ↗ Generar en editor
                                </button>
                            </div>
                        </div>
                    </div>
                </div>
            </div>

            <!-- Panel de resultados / historial -->
            <div class="flex-1 overflow-hidden flex flex-col px-4 pt-3 pb-4">

                <!-- Status bar -->
                <div x-show="statusMsg" class="mb-2 px-3 py-1.5 rounded text-xs font-medium"
                    :class="statusOk ? 'bg-green-50 text-green-700 border border-green-200' : 'bg-red-50 text-red-600 border border-red-200'"
                    x-text="statusMsg"></div>

                <!-- ── TAB: Resultados ── -->
                <div x-show="activeTab === 'results'" class="flex-1 overflow-auto border rounded-lg">
                    <!-- Sin resultados -->
                    <div x-show="!batchMode && results.length === 0 && !loadingQuery" class="flex flex-col items-center justify-center h-40 text-gray-400 text-sm gap-2">
                        <svg class="w-10 h-10 opacity-30" fill="none" stroke="currentColor" stroke-width="1.5" viewBox="0 0 24 24">
                            <path stroke-linecap="round" stroke-linejoin="round" d="M20.25 6.375c0 2.278-3.694 4.125-8.25 4.125S3.75 8.653 3.75 6.375m16.5 0c0-2.278-3.694-4.125-8.25-4.125S3.75 4.097 3.75 6.375m16.5 0v11.25c0 2.278-3.694 4.125-8.25 4.125s-8.25-1.847-8.25-4.125V6.375m16.5 5.625c0 2.278-3.694 4.125-8.25 4.125s-8.25-1.847-8.25-4.125"/>
                        </svg>
                        <span>Sin resultados. Ejecuta una consulta.</span>
                    </div>

                    <!-- Batch results tabs -->
                    <div x-show="batchMode && batchResults.length > 0" class="flex flex-col h-full">
                        <div class="flex items-center gap-1 px-2 py-1.5 border-b bg-gray-50 sticky top-0 overflow-x-auto shrink-0">
                            <template x-for="(br, i) in batchResults" :key="i">
                                <button @click="batchActiveIdx = i"
                                    class="px-2.5 py-1 text-[10px] rounded whitespace-nowrap font-mono transition flex items-center gap-1"
                                    :class="batchActiveIdx === i
                                        ? (br.status === 'error' ? 'bg-red-100 text-red-700 border border-red-200' : 'bg-[#8eb02f] text-white')
                                        : (br.status === 'error' ? 'bg-red-50 text-red-500 border border-red-100' : 'bg-white text-gray-600 border border-gray-200 hover:bg-gray-50')">
                                    <span x-text="'#' + (i+1)"></span>
                                    <span class="truncate max-w-[100px]" x-text="br.sql.substring(0, 30)"></span>
                                    <span x-show="br.status === 'ok'" class="text-green-500">✓</span>
                                    <span x-show="br.status === 'error'" class="text-red-500">✗</span>
                                </button>
                            </template>
                        </div>

                        <!-- Resultado activo del batch -->
                        <template x-for="(br, i) in batchResults" :key="i">
                            <div x-show="batchActiveIdx === i" class="flex-1 overflow-auto">
                                <div x-show="br.status === 'error'" class="p-4 text-xs text-red-600 bg-red-50 font-mono" x-text="br.error"></div>
                                <div x-show="br.status === 'ok' && (!br.columns || br.columns.length === 0)" class="p-4 text-xs text-green-700 bg-green-50">
                                    ✅ Consulta ejecutada — <span x-text="br.rows + ' filas afectadas'"></span> · <span x-text="br.duration"></span>
                                </div>
                                <table x-show="br.status === 'ok' && br.columns && br.columns.length > 0" class="table-auto w-full text-xs">
                                    <thead class="sticky top-0 bg-gray-50 z-10">
                                        <tr>
                                            <th class="py-2 px-3 border-b text-left font-semibold text-gray-500 w-10">#</th>
                                            <template x-for="col in br.columns" :key="col">
                                                <th class="py-2 px-3 border-b text-left font-semibold text-gray-600 whitespace-nowrap" x-text="col"></th>
                                            </template>
                                        </tr>
                                    </thead>
                                    <tbody class="divide-y divide-gray-100">
                                        <template x-for="(row, idx) in br.data" :key="idx">
                                            <tr class="hover:bg-gray-50">
                                                <td class="py-2 px-3 text-gray-400 select-none" x-text="idx+1"></td>
                                                <template x-for="col in br.columns" :key="col">
                                                    <td class="py-2 px-3 font-mono max-w-xs truncate" :title="nullStr(row[col])" x-text="nullStr(row[col])"></td>
                                                </template>
                                            </tr>
                                        </template>
                                    </tbody>
                                </table>
                                <div x-show="br.status === 'ok'" class="px-3 py-1.5 text-[10px] text-gray-400 border-t bg-gray-50">
                                    <span x-text="(br.data || []).length + ' filas'"></span> ·
                                    <span x-text="br.duration"></span>
                                </div>
                            </div>
                        </template>
                    </div>

                    <!-- Single results table (modo normal) -->
                    <table x-show="!batchMode && results.length > 0" class="table-auto w-full text-xs">
                        <thead class="sticky top-0 bg-gray-50 z-10">
                            <tr>
                                <th class="py-2 px-3 border-b text-left font-semibold text-gray-500 w-10">#</th>
                                <template x-for="col in columns" :key="col">
                                    <th class="py-2 px-3 border-b text-left font-semibold text-gray-600 whitespace-nowrap" x-text="col"></th>
                                </template>
                            </tr>
                        </thead>
                        <tbody class="divide-y divide-gray-100">
                            <template x-for="(row, idx) in results" :key="idx">
                                <tr class="hover:bg-gray-50">
                                    <td class="py-2 px-3 text-gray-400 select-none" x-text="idx+1"></td>
                                    <template x-for="col in columns" :key="col">
                                        <td class="py-2 px-3 font-mono max-w-xs truncate relative"
                                            :title="nullStr(row[col])"
                                            @dblclick="startEdit(idx, col, row, $event)">
                                            <div x-show="!(editCell && editCell.rowIdx === idx && editCell.col === col)" x-text="nullStr(row[col])"></div>
                                            <div x-show="editCell && editCell.rowIdx === idx && editCell.col === col" class="flex items-center gap-1">
                                                <input x-model="editValue" type="text"
                                                    @keydown.enter.prevent="saveEdit(idx, col)"
                                                    @keydown.escape.prevent="cancelEdit()"
                                                    @click.stop
                                                    class="w-full border border-[#8eb02f] rounded px-1.5 py-0.5 text-xs font-mono outline-none bg-white"
                                                    :class="editSaving ? 'opacity-50' : ''"
                                                    :disabled="editSaving"
                                                    x-ref="editInput">
                                                <button @click="saveEdit(idx, col)" :disabled="editSaving"
                                                    class="shrink-0 px-1 py-0.5 rounded bg-green-500 text-white text-[10px] hover:bg-green-600">✓</button>
                                                <button @click="cancelEdit()"
                                                    class="shrink-0 px-1 py-0.5 rounded bg-gray-300 text-gray-700 text-[10px] hover:bg-gray-400">✕</button>
                                            </div>
                                        </td>
                                    </template>
                                </tr>
                            </template>
                        </tbody>
                    </table>
                </div>

                <!-- ── TAB: Historial ── -->
                <div x-show="activeTab === 'history'" class="flex-1 overflow-auto border rounded-lg">
                    <div class="flex items-center justify-between px-3 py-2 border-b bg-gray-50 sticky top-0">
                        <span class="text-xs font-semibold text-gray-500">Historial de consultas</span>
                        <button @click="clearHistory()" x-show="history.length > 0"
                            class="text-xs text-red-500 hover:underline">Borrar historial</button>
                    </div>
                    <div x-show="history.length === 0" class="py-10 text-center text-xs text-gray-400">Sin historial para esta conexión.</div>
                    <template x-for="(h, i) in history" :key="h.ID">
                        <div class="px-3 py-2.5 border-b hover:bg-gray-50 group cursor-pointer"
                            @click="sqlText = h.sql">
                            <div class="flex items-start justify-between gap-2">
                                <pre class="text-xs font-mono text-gray-700 whitespace-pre-wrap break-all flex-1 max-h-12 overflow-hidden" x-text="h.sql"></pre>
                                <div class="shrink-0 flex items-center gap-2">
                                    <span class="px-1.5 py-0.5 rounded text-[10px] font-medium"
                                        :class="h.status === 'ok' ? 'bg-green-100 text-green-700' : 'bg-red-100 text-red-600'"
                                        x-text="h.status"></span>
                                    <span class="text-[10px] text-gray-400" x-text="h.duration_ms + 'ms'"></span>
                                </div>
                            </div>
                            <p x-show="h.error_msg" class="text-[10px] text-red-400 mt-0.5 truncate" x-text="h.error_msg"></p>
                            <p class="text-[10px] text-gray-400 mt-0.5" x-text="formatDate(h.executed_at)"></p>
                        </div>
                    </template>
                </div>

            </div>
        </div>
    </div>

    <!-- Modal AI Sugerencia -->
    <div x-show="aiModalOpen" x-cloak x-transition class="fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-50">
        <div @click.outside="aiModalOpen = false" class="bg-white rounded-xl shadow-2xl w-full max-w-2xl mx-4 overflow-hidden">
            <div class="flex items-center justify-between px-4 py-3 border-b bg-gray-50">
                <div class="flex items-center gap-2">
                    <svg class="w-4 h-4 text-purple-600" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
                        <path d="M12 3c.132 0 .263 0 .393 0a7.5 7.5 0 007.92 12.446A9 9 0 1112 2.992z"/>
                    </svg>
                    <span class="text-sm font-semibold text-gray-700">Sugerencia IA</span>
                    <span class="text-[10px] px-1.5 py-0.5 rounded bg-purple-100 text-purple-600 font-medium">AI</span>
                </div>
                <button @click="aiModalOpen = false" class="text-gray-400 hover:text-gray-600 text-lg leading-none">&times;</button>
            </div>
            <div class="p-4">
                <div class="mb-3">
                    <label class="text-[10px] text-gray-500 font-semibold uppercase tracking-wide block mb-1">Acción</label>
                    <div class="flex gap-1.5">
                        <template x-for="act in aiActions" :key="act.value">
                            <button @click="aiAction = act.value"
                                class="px-3 py-1.5 text-xs rounded border transition"
                                :class="aiAction === act.value ? 'bg-purple-600 text-white border-purple-600' : 'bg-white text-gray-600 border-gray-300 hover:bg-gray-50'"
                                x-text="act.label"></button>
                        </template>
                    </div>
                </div>
                <div class="border rounded-lg bg-gray-900 p-3 mb-3 max-h-40 overflow-y-auto">
                    <pre class="text-xs font-mono text-gray-100 whitespace-pre-wrap" x-text="aiModalSql"></pre>
                </div>
                <button @click="askAI()" :disabled="aiLoading || !sqlText.trim()"
                    class="w-full py-2 text-xs font-semibold text-white rounded-lg transition disabled:opacity-40 flex items-center justify-center gap-2"
                    :class="aiLoading ? 'bg-purple-400' : 'bg-purple-600 hover:bg-purple-700'">
                    <svg x-show="aiLoading" class="w-3.5 h-3.5 animate-spin" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
                        <path stroke-linecap="round" stroke-linejoin="round" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/>
                    </svg>
                    <span x-text="aiLoading ? 'Procesando…' : 'Obtener sugerencia'"></span>
                </button>
                <div x-show="aiSuggestion" class="mt-3 border rounded-lg overflow-hidden">
                    <div class="flex items-center justify-between px-3 py-2 bg-gray-50 border-b">
                        <span class="text-xs font-semibold text-gray-500">Resultado</span>
                        <button @click="applyAISuggestion()"
                            class="text-[10px] px-2 py-1 rounded bg-[#8eb02f] text-white hover:bg-[#6d8c24] transition font-semibold">
                            Aplicar al editor
                        </button>
                    </div>
                    <pre class="p-3 text-xs font-mono text-gray-800 whitespace-pre-wrap bg-white max-h-48 overflow-y-auto" x-text="aiSuggestion"></pre>
                </div>
            </div>
        </div>
    </div>

    <!-- Toast -->
    <div x-show="toast.show" x-cloak x-transition
        class="fixed bottom-4 right-4 z-[100] px-4 py-3 rounded shadow-lg text-sm text-white"
        :class="toast.type === 'error' ? 'bg-red-500' : 'bg-[#8eb02f]'"
        x-text="toast.msg"></div>
</div>

<script>
document.addEventListener('alpine:init', () => {
    Alpine.data('queryRunner', () => ({
        loading: false,
        loadingQuery: false,
        testLoading: false,
        selectedConxId: '',
        selectedDb: '',
        conexiones: [],
        databases: [],
        tables: [],
        sqlText: '',
        columns: [],
        results: [],
        history: [],
        activeTab: 'results',
        batchMode: false,
        batchResults: [],
        batchActiveIdx: 0,
        statusMsg: '',
        statusOk: true,
        testMsg: '',
        testOk: true,
        toast: { show: false, msg: '', type: 'ok' },
        isMongo: false,
        isPostgres: false,
        isRedis: false,
        selectedCollection: '',
        collectionFields: [],
        loadingFields: false,
        showUpdateBuilder: false,
        updateBuilder: { collection: '', field: '', value: '', filterField: '_id', filterValue: '""', op: '$set', multi: 'one' },

        // ── Autocomplete ──
        autocompleteVisible: false,
        autocompleteItems: [],
        autocompleteIdx: 0,
        autocompletePos: { left: 0, top: 0 },
        columnCache: {}, // table name -> columns array
        sqlKeywords: [
            'SELECT', 'FROM', 'WHERE', 'AND', 'OR', 'IN', 'NOT', 'NULL', 'AS', 'ON',
            'JOIN', 'LEFT', 'RIGHT', 'INNER', 'OUTER', 'CROSS', 'FULL',
            'GROUP', 'BY', 'ORDER', 'ASC', 'DESC', 'HAVING',
            'LIMIT', 'OFFSET', 'INSERT', 'INTO', 'VALUES',
            'UPDATE', 'SET', 'DELETE', 'CREATE', 'TABLE', 'ALTER', 'DROP', 'INDEX',
            'PRIMARY', 'KEY', 'FOREIGN', 'REFERENCES', 'CONSTRAINT', 'DEFAULT', 'UNIQUE',
            'CHECK', 'CASCADE', 'TRUNCATE', 'DISTINCT', 'COUNT', 'SUM', 'AVG', 'MIN', 'MAX',
            'BETWEEN', 'LIKE', 'ILIKE', 'EXISTS', 'UNION', 'ALL', 'INTERSECT', 'EXCEPT',
            'CASE', 'WHEN', 'THEN', 'ELSE', 'END', 'CAST', 'COALESCE', 'NULLIF',
            'TRUE', 'FALSE', 'GRANT', 'REVOKE', 'COMMIT', 'ROLLBACK', 'BEGIN', 'TRANSACTION',
            'IS', 'ANY', 'SOME', 'DATABASE', 'SCHEMA', 'USER', 'GRANT', 'REVOKE',
            'PRIVILEGES', 'IDENTIFIED', 'PASSWORD', 'FLUSH',
            'SHOW', 'DESCRIBE', 'EXPLAIN', 'WITH',
            'IF', 'EXISTS', 'NOT', 'FULL', 'OUTER', 'CROSS', 'NATURAL', 'USING',
        ],

        // ── Editable cells ──
        editCell: null,
        editValue: '',
        editSaving: false,

        // ── NL2SQL ──
        nl2sqlText: '',
        nl2sqlLoading: false,
        nl2sqlResult: '',

        // ── Typo detection ──
        typoSuggestions: [],
        aiLoading: false,
        aiModalOpen: false,
        aiModalSql: '',
        aiSuggestion: '',
        aiAction: 'correct',
        aiActions: [
            { value: 'correct', label: 'Corregir errores' },
            { value: 'complete', label: 'Completar consulta' },
            { value: 'optimize', label: 'Optimizar SQL' },
        ],

        // Redis helpers
        showRedisBuilder: false,
        redisBuilder: { cmd: 'GET', key: '', value: '', ttl: '3600', field: '' },
        redisHelpers: [
            { label: 'KEYS *',     cmd: 'KEYS *' },
            { label: 'DBSIZE',     cmd: 'DBSIZE' },
            { label: 'INFO',       cmd: 'INFO server' },
            { label: 'INFO memory',cmd: 'INFO memory' },
            { label: 'CONFIG GET maxmemory', cmd: 'CONFIG GET maxmemory' },
            { label: 'CONFIG GET save', cmd: 'CONFIG GET save' },
            { label: 'CLIENT LIST',cmd: 'CLIENT LIST' },
            { label: 'SLOWLOG GET',cmd: 'SLOWLOG GET 10' },
        ],

        // SQL DBA helpers
        showDbHelper: false,
        dbHelper: { db: '', user: '', pass: '', host: '%', op: 'full_setup' },
        get sqlHelpers() {
            if (this.isPostgres) return [
                { label: 'SHOW DATABASES',    cmd: 'SELECT datname FROM pg_database ORDER BY datname;' },
                { label: 'SHOW TABLES',        cmd: "SELECT table_name FROM information_schema.tables WHERE table_schema = 'public' ORDER BY table_name;" },
                { label: 'SHOW USERS',         cmd: "SELECT usename, usesuper, usecreatedb, usecreaterole FROM pg_catalog.pg_user ORDER BY usename;" },
                { label: 'SHOW GRANTS',        cmd: "SELECT grantee, table_name, privilege_type FROM information_schema.role_table_grants WHERE table_schema = 'public' ORDER BY grantee, table_name;" },
                { label: 'SHOW CONNECTIONS',   cmd: "SELECT pid, usename, application_name, state, query_start, LEFT(query,80) AS query FROM pg_stat_activity WHERE state IS NOT NULL;" },
                { label: 'DESC tabla',         cmd: "SELECT column_name, data_type, is_nullable, column_default FROM information_schema.columns WHERE table_name = 'nombre_tabla' ORDER BY ordinal_position;" },
                { label: 'SHOW INDEXES',       cmd: "SELECT indexname, indexdef FROM pg_indexes WHERE tablename = 'nombre_tabla';" },
                { label: 'SIZE db',            cmd: "SELECT pg_size_pretty(pg_database_size(current_database())) AS size;" },
            ];
            return [
                { label: 'SHOW DATABASES',     cmd: 'SHOW DATABASES;' },
                { label: 'SHOW TABLES',        cmd: 'SHOW TABLES;' },
                { label: 'SHOW USERS',         cmd: "SELECT User, Host, plugin FROM mysql.user ORDER BY User;" },
                { label: 'SHOW GRANTS',        cmd: 'SHOW GRANTS FOR CURRENT_USER;' },
                { label: 'SHOW PROCESSLIST',   cmd: 'SHOW FULL PROCESSLIST;' },
                { label: 'SHOW CREATE TABLE',  cmd: 'SHOW CREATE TABLE nombre_tabla;' },
                { label: 'DESC tabla',         cmd: 'DESCRIBE nombre_tabla;' },
                { label: 'ENGINE/CHARSET',     cmd: "SELECT table_name, engine, table_collation FROM information_schema.tables WHERE table_schema = DATABASE();" },
            ];
        },
        mongoExamples: [
            { label: 'find()', cmd: 'db.coleccion.find({})' },
            { label: 'findOne()', cmd: 'db.coleccion.findOne({})' },
            { label: 'count()', cmd: 'db.coleccion.countDocuments({})' },
            { label: 'insertOne()', cmd: 'db.coleccion.insertOne({"campo": "valor"})' },
            { label: 'updateOne()', cmd: 'db.coleccion.updateOne(\n  {"_id": ""},\n  {"$set": {"campo": "valor"}}\n)' },
            { label: 'updateMany()', cmd: 'db.coleccion.updateMany(\n  {},\n  {"$set": {"campo": "valor"}}\n)' },
            { label: '$unset', cmd: 'db.coleccion.updateOne(\n  {"_id": ""},\n  {"$unset": {"campo": ""}}\n)' },
            { label: '$inc', cmd: 'db.coleccion.updateOne(\n  {"_id": ""},\n  {"$inc": {"numero": 1}}\n)' },
            { label: '$push', cmd: 'db.coleccion.updateOne(\n  {"_id": ""},\n  {"$push": {"array": "nuevo_elemento"}}\n)' },
            { label: '$pull', cmd: 'db.coleccion.updateOne(\n  {"_id": ""},\n  {"$pull": {"array": "elemento"}}\n)' },
            { label: 'deleteOne()', cmd: 'db.coleccion.deleteOne({"_id": ""})' },
            { label: 'aggregate()', cmd: 'db.coleccion.aggregate([{"$group": {"_id": "$campo", "total": {"$sum": 1}}}])' },
        ],

        // ══════════════════════════════════════════════════════════════
        // INIT
        // ══════════════════════════════════════════════════════════════
        async init() {
            const params = new URLSearchParams(window.location.search);
            const preId = params.get('conx_db_id');
            const res = await axios.get('/app/query-runner/connections');
            this.conexiones = res.data.data || [];
            if (preId) {
                this.selectedConxId = parseInt(preId);
                await this.onConxChange();
            }
        },

        // ══════════════════════════════════════════════════════════════
        // AUTOCOMPLETE — con contexto WHERE/SET/ON/HAVING
        // ══════════════════════════════════════════════════════════════
        async onSqlInput() {
            if (!this.sqlText || this.isMongo || this.isRedis) {
                this.closeAutocomplete();
                this.typoSuggestions = [];
                return;
            }
            const ta = document.getElementById('sql-editor');
            if (!ta) return;
            const cursorPos = ta.selectionStart;
            const textBefore = this.sqlText.substring(0, cursorPos);
            const wordMatch = textBefore.match(/[\w.]+$/);
            const currentWord = wordMatch ? wordMatch[0] : '';

            // Prefetch columnas de TODAS las tablas de la query en paralelo
            this.prefetchTableColumns(this.sqlText);

            if (currentWord.length < 1) {
                this.closeAutocomplete();
                this.typoSuggestions = [];
                return;
            }

            const upper = currentWord.toUpperCase();

            // ── Detectar contexto del cursor ──
            // Contexto columna: cursor después de WHERE, AND, OR, ON, SET, HAVING, SELECT campo
            const contextUpper = textBefore.replace(/\s+/g, ' ').toUpperCase();
            const columnContextRe = /(?:WHERE|AND|OR|HAVING|ON|SET|,)\s+[\w.]*$/;
            const selectContextRe = /SELECT\s+(?:[\w.,\s]+,\s*)?[\w.]*$/;
            const isColumnContext = columnContextRe.test(contextUpper) || selectContextRe.test(contextUpper);

            const suggestions = [];

            if (isColumnContext) {
                // Prioridad 1: columnas de TODAS las tablas en el query
                const allTables = this.extractAllTablesFromQuery(this.sqlText);
                for (const tableName of allTables) {
                    const cols = this.columnCache[tableName] || [];
                    for (const col of cols) {
                        const colLower = col.name.toLowerCase();
                        if (colLower.startsWith(currentWord.toLowerCase()) && col.name !== currentWord) {
                            suggestions.push({
                                label: col.name,
                                type: 'col·' + (col.type || '').substring(0, 10),
                                priority: 0
                            });
                        }
                    }
                }
                // Prioridad 2: también keywords que aplican en este contexto
                const whereKws = ['IS', 'NOT', 'NULL', 'IN', 'LIKE', 'ILIKE', 'BETWEEN', 'AND', 'OR', 'EXISTS', 'TRUE', 'FALSE'];
                for (const kw of whereKws) {
                    if (kw.startsWith(upper) && kw !== upper) {
                        suggestions.push({ label: kw, type: 'keyword', priority: 1 });
                    }
                }
            } else {
                // Contexto general: keywords primero
                for (const kw of this.sqlKeywords) {
                    if (kw.startsWith(upper) && kw !== upper) {
                        suggestions.push({ label: kw, type: 'keyword', priority: 0 });
                    }
                }
                // Luego tablas (contexto FROM / JOIN / UPDATE / INTO)
                const tableContextRe = /(?:FROM|JOIN|UPDATE|INTO|TABLE)\s+[\w.]*$/i;
                if (tableContextRe.test(textBefore)) {
                    for (const t of this.tables) {
                        if (t.toLowerCase().startsWith(currentWord.toLowerCase()) && t !== currentWord) {
                            suggestions.push({ label: t, type: 'table', priority: 0 });
                        }
                    }
                } else {
                    // Fuera de contexto claro: tablas de menor prioridad
                    for (const t of this.tables) {
                        if (t.toLowerCase().startsWith(currentWord.toLowerCase()) && t !== currentWord) {
                            suggestions.push({ label: t, type: 'table', priority: 1 });
                        }
                    }
                }
                // Columnas de tabla detectada en contexto
                const tableCtx = this.detectTableContext(this.sqlText, cursorPos);
                if (tableCtx && this.columnCache[tableCtx]) {
                    for (const col of this.columnCache[tableCtx]) {
                        if (col.name.toLowerCase().startsWith(currentWord.toLowerCase()) && col.name !== currentWord) {
                            suggestions.push({ label: col.name, type: 'col·' + (col.type || '').substring(0, 10), priority: 1 });
                        }
                    }
                }
            }

            // Deduplicar y limitar
            const seen = new Set();
            const unique = suggestions.filter(s => {
                if (seen.has(s.label)) return false;
                seen.add(s.label);
                return true;
            }).slice(0, 14);

            if (unique.length > 0) {
                this.autocompleteItems = unique;
                this.autocompleteIdx = 0;
                this.autocompleteVisible = true;
                // Calcular posición debajo del cursor
                const lineHeight = 24;
                const charWidth = 8.4;
                const lines = textBefore.split('\n');
                const lineNum = lines.length;
                const colNum = lines[lines.length - 1].length;
                this.autocompletePos = {
                    left: Math.min(colNum * charWidth, 560),
                    top: lineNum * lineHeight + 4
                };
            } else {
                this.closeAutocomplete();
            }

            // Typo detection
            this.detectTypos(currentWord);
        },

        // Extrae TODAS las tablas mencionadas en FROM, JOIN, UPDATE, INTO del query
        extractAllTablesFromQuery(sql) {
            const found = [];
            const re = /(?:FROM|JOIN|UPDATE|INTO|TABLE)\s+`?(\w[\w\d_]*)`?/gi;
            let m;
            while ((m = re.exec(sql)) !== null) {
                const t = m[1];
                if (!found.includes(t) && this.tables.includes(t)) found.push(t);
            }
            return found;
        },

        onSqlKeydown(e) {
            if (!this.autocompleteVisible) return;
            if (e.key === 'ArrowDown') {
                e.preventDefault();
                this.autocompleteIdx = Math.min(this.autocompleteIdx + 1, this.autocompleteItems.length - 1);
            } else if (e.key === 'ArrowUp') {
                e.preventDefault();
                this.autocompleteIdx = Math.max(this.autocompleteIdx - 1, 0);
            } else if (e.key === 'Enter' || e.key === 'Tab') {
                if (this.autocompleteItems[this.autocompleteIdx]) {
                    e.preventDefault();
                    this.selectAutocomplete(this.autocompleteIdx);
                }
            } else if (e.key === 'Escape') {
                this.closeAutocomplete();
            }
        },

        selectAutocomplete(idx) {
            const item = this.autocompleteItems[idx];
            if (!item) return;
            const ta = document.getElementById('sql-editor');
            if (!ta) return;
            const cursorPos = ta.selectionStart;
            const text = this.sqlText.substring(0, cursorPos);
            const words = text.split(/[\s,;()\n]+/);
            const currentWord = words[words.length - 1] || '';
            const before = this.sqlText.substring(0, cursorPos - currentWord.length);
            const after = this.sqlText.substring(cursorPos);
            this.sqlText = before + item.label.toLowerCase() + after;
            this.closeAutocomplete();
            // Move cursor after inserted word
            const newPos = before.length + item.label.length;
            requestAnimationFrame(() => {
                ta.focus();
                ta.setSelectionRange(newPos, newPos);
            });
        },

        closeAutocomplete() {
            this.autocompleteVisible = false;
            this.autocompleteItems = [];
            this.autocompleteIdx = -1;
        },

        // Detecta qué tabla se está consultando basado en el contexto del cursor
        detectTableContext(sql, cursorPos) {
            const textBefore = sql.substring(0, cursorPos);
            const upper = textBefore.toUpperCase();
            // Buscar patrones: FROM tabla, JOIN tabla, UPDATE tabla, INSERT INTO tabla
            const fromMatch = upper.match(/(?:FROM|JOIN|UPDATE|INTO)\s+([A-Z_][A-Z0-9_]*)\s*$/);
            if (fromMatch) {
                const table = fromMatch[1].toLowerCase();
                // Verificar que existe en tables
                for (const t of this.tables) {
                    if (t.toLowerCase() === table) return t;
                }
            }
            // También buscar cualquier tabla mencionada antes del cursor
            for (const t of this.tables) {
                const idx = textBefore.toLowerCase().lastIndexOf(t.toLowerCase());
                if (idx >= 0 && idx < cursorPos) return t;
            }
            return null;
        },

        // Precarga columnas de tablas mencionadas en la query
        async prefetchTableColumns(sql) {
            if (!this.selectedConxId || !this.selectedDb) return;
            const tableRegex = /(?:FROM|JOIN|UPDATE|INTO|TABLE)\s+`?(\w[\w\d_]*)`?/gi;
            let match;
            const tablesToFetch = new Set();
            while ((match = tableRegex.exec(sql)) !== null) {
                tablesToFetch.add(match[1]);
            }
            for (const table of tablesToFetch) {
                if (!this.columnCache[table] && this.tables.includes(table)) {
                    try {
                        const res = await axios.get('/app/query-runner/columns', {
                            params: { conx_db_id: this.selectedConxId, db: this.selectedDb, table }
                        });
                        this.columnCache[table] = res.data.data || [];
                    } catch (_) {}
                }
            }
        },

        // ══════════════════════════════════════════════════════════════
        // TYPO DETECTION
        // ══════════════════════════════════════════════════════════════
        detectTypos(word) {
            if (!word || word.length < 2) {
                this.typoSuggestions = [];
                return;
            }
            const upper = word.toUpperCase();
            const suggestions = [];
            // Levenshtein distance check against keywords
            for (const kw of this.sqlKeywords) {
                if (kw === upper) continue;
                const dist = this.levenshtein(upper, kw);
                if (dist === 1 || (dist === 2 && kw.length > 4)) {
                    suggestions.push(kw);
                }
                if (suggestions.length >= 3) break;
            }
            // Check against table names
            for (const t of this.tables) {
                const tUpper = t.toUpperCase();
                if (tUpper === upper) continue;
                const dist = this.levenshtein(upper, tUpper);
                if (dist <= 2) {
                    suggestions.push(t);
                }
                if (suggestions.length >= 5) break;
            }
            this.typoSuggestions = suggestions;
        },

        levenshtein(a, b) {
            const m = a.length, n = b.length;
            const dp = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0));
            for (let i = 0; i <= m; i++) dp[i][0] = i;
            for (let j = 0; j <= n; j++) dp[0][j] = j;
            for (let i = 1; i <= m; i++) {
                for (let j = 1; j <= n; j++) {
                    const cost = a[i - 1] === b[j - 1] ? 0 : 1;
                    dp[i][j] = Math.min(dp[i - 1][j] + 1, dp[i][j - 1] + 1, dp[i - 1][j - 1] + cost);
                }
            }
            return dp[m][n];
        },

        applyTypoFix(correctWord) {
            const ta = document.getElementById('sql-editor');
            if (!ta) return;
            const cursorPos = ta.selectionStart;
            const text = this.sqlText.substring(0, cursorPos);
            const words = text.split(/[\s,;()\n]+/);
            const currentWord = words[words.length - 1] || '';
            const before = this.sqlText.substring(0, cursorPos - currentWord.length);
            const after = this.sqlText.substring(cursorPos);
            this.sqlText = before + correctWord.toLowerCase() + after;
            this.typoSuggestions = [];
            const newPos = before.length + correctWord.length;
            requestAnimationFrame(() => {
                ta.focus();
                ta.setSelectionRange(newPos, newPos);
            });
        },

        // ══════════════════════════════════════════════════════════════
        // EDITABLE CELLS
        // ══════════════════════════════════════════════════════════════
        startEdit(rowIdx, col, row, event) {
            if (this.isMongo || this.isRedis) return;
            // Extract table name from the query
            const table = this.extractTableFromQuery();
            if (!table) {
                this.showToast('No se pudo detectar la tabla para editar. La consulta debe incluir FROM.', 'error');
                return;
            }
            // Find primary key column (try 'id', 'ID', or first column)
            const pkCol = this.detectPKColumn();
            if (!pkCol || row[pkCol] === undefined || row[pkCol] === null) {
                this.showToast('No se pudo detectar la llave primaria para editar.', 'error');
                return;
            }
            this.editCell = { rowIdx, col, table, pkCol, pkValue: String(row[pkCol]) };
            this.editValue = row[col] === null || row[col] === undefined ? '' : String(row[col]);
            requestAnimationFrame(() => {
                const input = this.$refs?.editInput;
                if (input) input.focus();
                else {
                    // Fallback: find the input element
                    const inputs = document.querySelectorAll('[x-ref="editInput"]');
                    if (inputs.length > 0) inputs[inputs.length - 1].focus();
                }
            });
        },

        async saveEdit(rowIdx, col) {
            if (!this.editCell || this.editSaving) return;
            this.editSaving = true;
            try {
                const res = await axios.post('/app/query-runner/update-cell', {
                    conx_db_id: parseInt(this.selectedConxId),
                    database: this.selectedDb,
                    table: this.editCell.table,
                    column: col,
                    pk_column: this.editCell.pkCol,
                    pk_value: this.editCell.pkValue,
                    value: this.editValue
                });
                if (res.data.error) {
                    this.showToast('Error: ' + res.data.error, 'error');
                } else {
                    this.showToast('✓ Celda actualizada (' + res.data.affected + ' fila(s))');
                    // Update local result
                    if (this.results[rowIdx]) {
                        this.results[rowIdx][col] = this.editValue;
                    }
                }
            } catch (e) {
                this.showToast('Error al actualizar: ' + (e.response?.data?.error || e.message), 'error');
            }
            this.editSaving = false;
            this.editCell = null;
            this.editValue = '';
        },

        cancelEdit() {
            this.editCell = null;
            this.editValue = '';
        },

        extractTableFromQuery() {
            const upper = this.sqlText.toUpperCase();
            // FROM tabla, JOIN tabla, UPDATE tabla
            const match = upper.match(/(?:FROM|JOIN|UPDATE|INTO)\s+`?(\w[\w\d_]*)`?/);
            if (match) {
                const tableName = match[1].toLowerCase();
                // Check against known tables
                for (const t of this.tables) {
                    if (t.toLowerCase() === tableName) return t;
                }
                return match[1];
            }
            return null;
        },

        detectPKColumn() {
            // Common primary key names
            const pkCandidates = ['id', 'ID', 'Id', '_id', 'codigo', 'code', 'uuid', 'ID_'];
            for (const col of this.columns) {
                for (const pk of pkCandidates) {
                    if (col === pk) return col;
                }
            }
            // Fallback: first column
            return this.columns[0] || null;
        },

        // ══════════════════════════════════════════════════════════════
        // AI SUGGESTION
        // ══════════════════════════════════════════════════════════════
        openAI() {
            this.aiModalOpen = true;
            this.aiModalSql = this.sqlText;
            this.aiSuggestion = '';
        },

        async askAI() {
            if (!this.sqlText.trim()) return;
            if (!this.aiModalOpen) {
                this.aiModalOpen = true;
                this.aiModalSql = this.sqlText;
                this.aiSuggestion = '';
                return;
            }
            this.aiLoading = true;
            this.aiSuggestion = '';
            try {
                const res = await axios.post('/app/query-runner/suggest', {
                    conx_db_id: parseInt(this.selectedConxId) || 0,
                    database: this.selectedDb,
                    sql: this.aiModalSql || this.sqlText,
                    action: this.aiAction
                });
                if (res.data.suggestion) {
                    this.aiSuggestion = res.data.suggestion;
                } else {
                    this.showToast('No se pudo generar sugerencia', 'error');
                }
            } catch (e) {
                this.showToast('Error IA: ' + (e.response?.data?.error || e.message), 'error');
            }
            this.aiLoading = false;
        },

        applyAISuggestion() {
            if (this.aiSuggestion) {
                this.sqlText = this.aiSuggestion;
                this.aiModalOpen = false;
                this.showToast('✓ Sugerencia aplicada al editor');
            }
        },

        // ══════════════════════════════════════════════════════════════
        // NL2SQL — Lenguaje natural a SQL
        // ══════════════════════════════════════════════════════════════
        async generateNL2SQL() {
            if (!this.nl2sqlText.trim() || this.nl2sqlLoading) return;
            this.nl2sqlLoading = true;
            this.nl2sqlResult = '';

            // Construir schema de tablas disponibles para dar contexto a la IA
            const schemaPayload = [];
            for (const tableName of this.tables) {
                const cols = this.columnCache[tableName] || [];
                schemaPayload.push({
                    table: tableName,
                    columns: cols.map(c => ({ name: c.name, type: c.type || '' }))
                });
                // Si no tenemos columnas cargadas aún, intentar cargarlas
                if (cols.length === 0 && this.selectedConxId && this.selectedDb) {
                    try {
                        const res = await axios.get('/app/query-runner/columns', {
                            params: { conx_db_id: this.selectedConxId, db: this.selectedDb, table: tableName }
                        });
                        const loaded = res.data.data || [];
                        this.columnCache[tableName] = loaded;
                        schemaPayload[schemaPayload.length - 1].columns = loaded.map(c => ({ name: c.name, type: c.type || '' }));
                    } catch (_) {}
                }
            }

            try {
                const res = await axios.post('/app/query-runner/nl2sql', {
                    conx_db_id: parseInt(this.selectedConxId) || 0,
                    database: this.selectedDb,
                    text: this.nl2sqlText,
                    schema: schemaPayload
                });
                if (res.data.suggestion) {
                    this.nl2sqlResult = res.data.suggestion;
                } else if (res.data.error) {
                    this.showToast(res.data.error, 'error');
                }
            } catch (e) {
                const msg = e.response?.data?.error || e.message;
                this.showToast('Error NL2SQL: ' + msg, 'error');
            }
            this.nl2sqlLoading = false;
        },

        applyNL2SQL() {
            if (this.nl2sqlResult) {
                this.sqlText = this.nl2sqlResult;
                this.nl2sqlResult = '';
                this.nl2sqlText = '';
                document.getElementById('sql-editor')?.focus();
                this.showToast('SQL aplicado al editor');
            }
        },

        // ══════════════════════════════════════════════════════════════
        // CORE QUERY RUNNER
        // ══════════════════════════════════════════════════════════════
        async onConxChange() {
            this.databases = [];
            this.tables = [];
            this.selectedDb = '';
            this.results = [];
            this.columns = [];
            this.statusMsg = '';
            this.history = [];
            this.isMongo = false;
            this.isRedis = false;
            this.selectedCollection = '';
            this.collectionFields = [];
            this.columnCache = {};
            if (!this.selectedConxId) return;
            const conx = this.conexiones.find(c => c.ID == this.selectedConxId);
            const dbType = conx?.tipo_db?.nombre?.toLowerCase() || '';
            this.isMongo    = dbType.includes('mongo');
            this.isPostgres = dbType.includes('postgres');
            this.isRedis    = dbType.includes('redis') || dbType.includes('valkey');
            try {
                const res = await axios.get('/app/query-runner/databases?conx_db_id=' + this.selectedConxId);
                this.databases = res.data.data || [];
            } catch (e) {
                this.showToast(e.response?.data?.error || 'Error al cargar DBs', 'error');
            }
            await this.loadHistory();
        },

        async loadTables() {
            this.tables = [];
            this.selectedCollection = '';
            this.collectionFields = [];
            if (!this.selectedDb) return;
            try {
                const res = await axios.get('/app/query-runner/tables?conx_db_id=' + this.selectedConxId + '&db=' + encodeURIComponent(this.selectedDb));
                this.tables = res.data.data || [];
            } catch (e) {
                this.showToast(e.response?.data?.error || 'Error al cargar tablas', 'error');
            }
        },

        async testConn() {
            this.testLoading = true;
            this.testMsg = '';
            try {
                await axios.get('/app/query-runner/test?conx_db_id=' + this.selectedConxId);
                this.testOk = true;
                this.testMsg = '✓ Conexión exitosa';
            } catch (e) {
                this.testOk = false;
                this.testMsg = '✗ ' + (e.response?.data?.error || 'Error de conexión');
            }
            this.testLoading = false;
            setTimeout(() => this.testMsg = '', 4000);
        },

        async runQuery() {
            if (!this.selectedConxId || !this.sqlText.trim()) return;
            if (this.batchMode) {
                await this.runBatch();
                return;
            }
            this.loadingQuery = true;
            this.statusMsg = '';
            this.results = [];
            this.columns = [];
            this.editCell = null;
            this.closeAutocomplete();
            try {
                const res = await axios.post('/app/query-runner/run', {
                    conx_db_id: parseInt(this.selectedConxId),
                    database: this.selectedDb,
                    sql: this.sqlText
                });
                const data = res.data;
                if (data.error) {
                    this.statusOk = false;
                    this.statusMsg = '✗ ' + data.error;
                } else if (data.is_select) {
                    this.columns = data.columns || [];
                    this.results = data.rows || [];
                    this.statusOk = true;
                    this.statusMsg = `✓ ${this.results.length} fila(s) — ${data.duration_ms}ms`;
                } else {
                    this.statusOk = true;
                    this.statusMsg = `✓ ${data.affected_rows} fila(s) afectadas — ${data.duration_ms}ms`;
                }
            } catch (e) {
                this.statusOk = false;
                this.statusMsg = '✗ ' + (e.response?.data?.error || 'Error al ejecutar');
            }
            this.loadingQuery = false;
            await this.loadHistory();
        },

        // ── Batch mode ──────────────────────────────────────────────
        extractSQLStatements(text) {
            // Split by semicolons, trim whitespace, remove empty
            return text
                .split(';')
                .map(s => s.trim())
                .filter(s => s.length > 0 && !s.toUpperCase().startsWith('--') && !s.startsWith('#'));
        },

        uploadSQLFile(event) {
            const file = event.target.files[0];
            if (!file) return;
            const reader = new FileReader();
            reader.onload = (e) => {
                const content = e.target.result;
                this.sqlText = content;
                this.batchMode = true;
                this.showToast(`📁 Archivo "${file.name}" cargado (${(content.length / 1024).toFixed(1)} KB)`);
            };
            reader.readAsText(file);
            // Reset input so re-selecting same file triggers change
            event.target.value = '';
        },

        async runBatch() {
            if (!this.selectedConxId) return;
            const sqls = this.extractSQLStatements(this.sqlText);
            if (sqls.length === 0) {
                this.showToast('No se encontraron sentencias SQL válidas', 'error');
                return;
            }
            this.loadingQuery = true;
            this.statusMsg = '';
            this.batchResults = [];
            this.batchActiveIdx = 0;
            this.activeTab = 'results';
            try {
                const formData = new FormData();
                formData.append('conx_db_id', this.selectedConxId);
                formData.append('database', this.selectedDb || '');
                sqls.forEach((sql, i) => formData.append('sqls[]', sql));
                const res = await axios.post('/app/query-runner/run-batch', formData, {
                    headers: { 'Content-Type': 'multipart/form-data' }
                });
                this.batchResults = res.data.results || [];
                const okCount = this.batchResults.filter(r => r.status === 'ok').length;
                const errCount = this.batchResults.filter(r => r.status === 'error').length;
                this.statusOk = errCount === 0;
                this.statusMsg = `✓ ${okCount} consultas ejecutadas, ${errCount} con error — ${res.data.duration || ''}`;
            } catch (e) {
                this.statusOk = false;
                this.statusMsg = '✗ ' + (e.response?.data?.error || 'Error al ejecutar batch');
                if (e.response?.data?.results) {
                    this.batchResults = e.response.data.results;
                }
            }
            this.loadingQuery = false;
        },

        async loadHistory() {
            if (!this.selectedConxId) return;
            try {
                const res = await axios.get('/app/query-runner/history?conx_db_id=' + this.selectedConxId);
                this.history = res.data.data || [];
            } catch (_) {}
        },

        async clearHistory() {
            if (!confirm('¿Borrar todo el historial de esta conexión?')) return;
            try {
                await axios.delete('/app/query-runner/history?conx_db_id=' + this.selectedConxId);
                this.history = [];
                this.showToast('Historial borrado');
            } catch (e) {
                this.showToast('Error al borrar historial', 'error');
            }
        },

        async exportData(fmt) {
            if (!this.sqlText.trim()) return;
            try {
                const res = await axios.post('/app/query-runner/export/' + fmt, {
                    conx_db_id: parseInt(this.selectedConxId),
                    database: this.selectedDb,
                    sql: this.sqlText
                }, { responseType: 'blob' });
                const mime = fmt === 'csv' ? 'text/csv' : 'application/json';
                const blob = new Blob([res.data], { type: mime });
                const url = URL.createObjectURL(blob);
                const a = document.createElement('a');
                a.href = url;
                a.download = 'query_result.' + fmt;
                a.click();
                URL.revokeObjectURL(url);
            } catch (e) {
                this.showToast('Error al exportar', 'error');
            }
        },

        insertTable(name) {
            let sel;
            if (this.isMongo) {
                sel = `db.${name}.find({})`;
            } else if (this.isRedis) {
                sel = `TYPE ${name}\nTTL ${name}\nGET ${name}`;
            } else {
                sel = `SELECT * FROM ${name} LIMIT 100;`;
            }
            this.sqlText = sel;
            document.getElementById('sql-editor')?.focus();
            this.runQuery();
        },

        async loadCollectionFields(name) {
            this.selectedCollection = name;
            this.collectionFields = [];
            if (!this.selectedConxId || !name) return;
            this.loadingFields = true;
            try {
                const res = await axios.post('/app/query-runner/run', {
                    conx_db_id: parseInt(this.selectedConxId),
                    database: this.selectedDb,
                    sql: `db.${name}.findOne({})`
                });
                const rows = res.data?.rows;
                if (rows && rows.length > 0) {
                    this.collectionFields = Object.entries(rows[0]).map(([k, v]) => ({
                        name: k,
                        type: v === null ? 'null' : Array.isArray(v) ? 'array' : typeof v,
                        preview: v === null ? 'null' : Array.isArray(v) ? `[${v.length} elem]` : String(v).substring(0, 30),
                        sample: v
                    }));
                }
            } catch (_) {}
            this.loadingFields = false;
        },

        buildUpdateField(fieldName) {
            const col = this.selectedCollection || 'coleccion';
            const f = this.collectionFields.find(f => f.name === fieldName);
            let sampleVal = '"nuevo_valor"';
            if (f && f.sample !== null && f.sample !== undefined) {
                if (typeof f.sample === 'number') sampleVal = String(f.sample);
                else if (typeof f.sample === 'boolean') sampleVal = String(f.sample);
                else if (Array.isArray(f.sample)) sampleVal = '[]';
                else if (typeof f.sample === 'object') sampleVal = '{}';
                else {
                    const safe = String(f.sample).replace(/\\/g, '\\\\').replace(/"/g, '\\"').substring(0, 50);
                    sampleVal = `"${safe}"`;
                }
            }
            this.sqlText = `db.${col}.updateOne(\n  { "_id": "" },\n  { "$set": { "${fieldName}": ${sampleVal} } }\n)`;
            document.getElementById('sql-editor')?.focus();
        },

        toJsonValue(raw) {
            const s = (raw || '').trim();
            if (s === '') return '""';
            if (/^".*"$/.test(s)) return s;
            if (s === 'true' || s === 'false' || s === 'null') return s;
            if (/^-?\d+(\.\d+)?([eE][+-]?\d+)?$/.test(s)) return s;
            if ((s.startsWith('{') && s.endsWith('}')) || (s.startsWith('[') && s.endsWith(']'))) return s;
            const escaped = s.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
            return `"${escaped}"`;
        },

        generateUpdate() {
            const col = this.updateBuilder.collection || this.selectedCollection || 'coleccion';
            const field = this.updateBuilder.field || 'campo';
            const rawValue = this.updateBuilder.value || '"nuevo_valor"';
            const filterField = this.updateBuilder.filterField || '_id';
            const rawFilter = this.updateBuilder.filterValue || '""';
            const op = this.updateBuilder.op || '$set';
            const method = this.updateBuilder.multi === 'many' ? 'updateMany' : 'updateOne';
            const value = this.toJsonValue(rawValue);
            const filterValue = this.toJsonValue(rawFilter);
            let updateDoc;
            if (op === '$unset') updateDoc = `{ "${field}": "" }`;
            else updateDoc = `{ "${field}": ${value} }`;
            this.sqlText = `db.${col}.${method}(\n  { "${filterField}": ${filterValue} },\n  { "${op}": ${updateDoc} }\n)`;
        },

        generateDbHelper() {
            const db   = this.dbHelper.db   || 'nombre_bd';
            const user = this.dbHelper.user || 'usuario_app';
            const host = this.dbHelper.host || '%';
            const pass = this.dbHelper.pass || 'Contraseña123!';
            let map;

            if (this.isPostgres) {
                const createDb   = `CREATE DATABASE "${db}"\n  WITH ENCODING 'UTF8'\n  LC_COLLATE = 'en_US.UTF-8'\n  LC_CTYPE   = 'en_US.UTF-8'\n  TEMPLATE   = template0;`;
                const createUser = `DO $$\nBEGIN\n  IF NOT EXISTS (SELECT FROM pg_catalog.pg_roles WHERE rolname = '${user}') THEN\n    CREATE USER "${user}" WITH PASSWORD '${pass}';\n  END IF;\nEND\n$$;`;
                const grant      = `GRANT ALL PRIVILEGES ON DATABASE "${db}" TO "${user}";\nGRANT ALL ON SCHEMA public TO "${user}";\nALTER DEFAULT PRIVILEGES IN SCHEMA public\n  GRANT ALL ON TABLES TO "${user}";\nALTER DEFAULT PRIVILEGES IN SCHEMA public\n  GRANT ALL ON SEQUENCES TO "${user}";`;
                const revoke     = `REVOKE ALL PRIVILEGES ON DATABASE "${db}" FROM "${user}";\nREVOKE ALL ON SCHEMA public FROM "${user}";`;
                const showGrants = `SELECT grantee, privilege_type, table_name\nFROM information_schema.role_table_grants\nWHERE grantee = '${user}'\nORDER BY table_name;`;
                const dropUser   = `DROP USER IF EXISTS "${user}";`;
                const dropDb     = `DROP DATABASE IF EXISTS "${db}";`;
                map = {
                    full_setup:  `-- 1. Crear base de datos\n${createDb}\n\n-- 2. Crear usuario (si no existe)\n${createUser}\n\n-- 3. Dar permisos totales\n${grant}`,
                    create_db:   createDb,
                    create_user: createUser,
                    grant:       grant,
                    revoke:      revoke,
                    show_grants: showGrants,
                    drop_user:   dropUser,
                    drop_db:     dropDb,
                };
            } else {
                const createDb   = `CREATE DATABASE IF NOT EXISTS \`${db}\`\n  CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;`;
                const createUser = `CREATE USER IF NOT EXISTS '${user}'@'${host}' IDENTIFIED BY '${pass}';`;
                const grant      = `GRANT ALL PRIVILEGES ON \`${db}\`.* TO '${user}'@'${host}';\nFLUSH PRIVILEGES;`;
                const revoke     = `REVOKE ALL PRIVILEGES ON \`${db}\`.* FROM '${user}'@'${host}';\nFLUSH PRIVILEGES;`;
                const showGrants = `SHOW GRANTS FOR '${user}'@'${host}';`;
                const dropUser   = `DROP USER IF EXISTS '${user}'@'${host}';\nFLUSH PRIVILEGES;`;
                const dropDb     = `DROP DATABASE IF EXISTS \`${db}\`;`;
                map = {
                    full_setup:  `-- 1. Crear base de datos\n${createDb}\n\n-- 2. Crear usuario\n${createUser}\n\n-- 3. Dar permisos totales\n${grant}`,
                    create_db:   createDb,
                    create_user: createUser,
                    grant:       grant,
                    revoke:      revoke,
                    show_grants: showGrants,
                    drop_user:   dropUser,
                    drop_db:     dropDb,
                };
            }

            this.sqlText = map[this.dbHelper.op] || '';
            document.getElementById('sql-editor')?.focus();
        },

        clearEditor() {
            this.sqlText = '';
            this.results = [];
            this.columns = [];
            this.statusMsg = '';
            this.closeAutocomplete();
            this.typoSuggestions = [];
            this.editCell = null;
        },

        generateRedisCmd() {
            const { cmd, key, value, ttl, field } = this.redisBuilder;
            const k = key || 'mi_clave';
            let sql = '';
            switch (cmd) {
                case 'GET':      sql = `GET ${k}`; break;
                case 'SET':      sql = `SET ${k} "${value || 'valor'}"`; break;
                case 'SETEX':    sql = `SETEX ${k} ${ttl || 3600} "${value || 'valor'}"`; break;
                case 'DEL':      sql = `DEL ${k}`; break;
                case 'EXISTS':   sql = `EXISTS ${k}`; break;
                case 'TTL':      sql = `TTL ${k}`; break;
                case 'EXPIRE':   sql = `EXPIRE ${k} ${ttl || 3600}`; break;
                case 'TYPE':     sql = `TYPE ${k}`; break;
                case 'KEYS':     sql = `KEYS ${k}`; break;
                case 'SCAN':     sql = `SCAN 0 MATCH ${k} COUNT 100`; break;
                case 'HSET':     sql = `HSET ${k} ${field || 'campo'} "${value || 'valor'}"`; break;
                case 'HGET':     sql = `HGET ${k} ${field || 'campo'}`; break;
                case 'HGETALL':  sql = `HGETALL ${k}`; break;
                case 'HDEL':     sql = `HDEL ${k} ${field || 'campo'}`; break;
                case 'LPUSH':    sql = `LPUSH ${k} "${value || 'elemento'}"`; break;
                case 'LRANGE':   sql = `LRANGE ${k} 0 -1`; break;
                case 'SADD':     sql = `SADD ${k} "${value || 'miembro'}"`; break;
                case 'SMEMBERS': sql = `SMEMBERS ${k}`; break;
                case 'ZADD':     sql = `ZADD ${k} 1 "${value || 'miembro'}"`; break;
                case 'ZRANGE':   sql = `ZRANGE ${k} 0 -1 WITHSCORES`; break;
                case 'DBSIZE':   sql = `DBSIZE`; break;
                case 'FLUSHDB':  sql = `FLUSHDB`; break;
                case 'INFO':     sql = `INFO server`; break;
                case 'CONFIG GET': sql = `CONFIG GET maxmemory`; break;
                default:         sql = `${cmd} ${k}`;
            }
            this.sqlText = sql;
            document.getElementById('sql-editor')?.focus();
        },

        nullStr(v) { return v === null || v === undefined ? 'NULL' : String(v); },

        formatDate(d) {
            if (!d) return '';
            return new Date(d).toLocaleString();
        },

        showToast(msg, type = 'ok') {
            this.toast = { show: true, msg, type };
            setTimeout(() => this.toast.show = false, 3500);
        }
    }));
});
</script>
