<script setup>
import { onMounted, reactive, ref, computed, watch } from 'vue';
import { Button, Badge, Input, Card, CardHeader, CardTitle, CardActions, CardContent, Textarea, DropdownMenu, DropdownMenuItem, DropdownMenuSeparator } from '@/components/ui';
import EventCard from '@/components/EventCard.vue';
import UserCard from '@/components/UserCard.vue';
import DispositivoCard from '@/components/DispositivoCard.vue';
import Modal from '@/components/Modal.vue';
import UserForm from '@/components/UserForm.vue';
import RawDbViewer from '@/components/RawDbViewer.vue';
import VlanForm from '@/components/VlanForm.vue';
import DeviceForm from '@/components/DeviceForm.vue';
import Toast from '@/components/Toast.vue';
import UserDropdown from '@/components/auth/UserDropdown.vue';
import { createToastSystem, useToast } from '@/composables/useToast.js';

// Initialize toast system
createToastSystem();
const { toast } = useToast();

const users = ref([]);
const requests = ref([]);
const loading = reactive({ users: false, requests: false, sessions: false });
const devices = ref([]);
const sessions = ref([]);
const sessionStats = ref({ active: 0, stopped: 0, stale: 0, total: 0 });
const userExpanded = reactive({});
const deviceExpanded = reactive({});

// Helper para detectar errores de autenticación
function isAuthError(error) {
  return error instanceof TypeError && error.message.includes('fetch');
}

function handleAuthError() {
  console.warn('Sesión expirada o error de autenticación, redirigiendo...');
  window.location.reload();
}

const showEventFilters = ref(false);
const showUserFilters = ref(false);
const eventFilters = reactive({ text: '', type: '' });
const userFilters = reactive({ text: '', status: '' });
const sidebarCollapsed = ref(false);
const mainCollapsed = ref(false);
const layoutMode = ref('user');
const theme = ref(localStorage.getItem('theme') || 'dark');
const statusText = ref('OK');
const showInfoModal = ref(false);

async function fetchUsers() {
  loading.users = true;
  try {
    const res = await fetch('/api/users');
    if (!res.ok) throw new Error(`HTTP ${res.status}: ${res.statusText}`);
    const data = await res.json();
    const newItems = data.items || [];
    // Solo actualizar si los datos cambiaron para evitar re-renders innecesarios
    if (JSON.stringify(newItems) !== JSON.stringify(users.value)) {
      users.value = newItems;
    }
  } catch (error) {
    if (isAuthError(error)) handleAuthError();
    else console.error('Error fetching users:', error);
  } finally { loading.users = false; }
}

async function fetchRequests() {
  loading.requests = true;
  try {
    const res = await fetch('/api/requests');
    if (!res.ok) throw new Error(`HTTP ${res.status}: ${res.statusText}`);
    const data = await res.json();
    requests.value = data.items || [];
  } catch (error) {
    if (isAuthError(error)) handleAuthError();
    else console.error('Error fetching requests:', error);
  } finally { loading.requests = false; }
}

async function fetchDevices() {
  try {
    const res = await fetch('/api/devices');
    if (!res.ok) throw new Error(`HTTP ${res.status}: ${res.statusText}`);
    const data = await res.json();
    const newItems = data.items || [];
    // Solo actualizar si los datos cambiaron para evitar re-renders innecesarios
    if (JSON.stringify(newItems) !== JSON.stringify(devices.value)) {
      devices.value = newItems;
    }
  } catch (error) {
    if (isAuthError(error)) handleAuthError();
    else console.error('Error fetching devices:', error);
  }
}

async function fetchSessions() {
  loading.sessions = true;
  try {
    const res = await fetch('/api/sessions');
    if (!res.ok) throw new Error(`HTTP ${res.status}: ${res.statusText}`);
    const data = await res.json();
    sessions.value = data.items || [];
    sessionStats.value = data.stats || { active: 0, stopped: 0, stale: 0, total: 0 };
  } catch (error) {
    if (isAuthError(error)) handleAuthError();
    else console.error('Error fetching sessions:', error);
  } finally {
    loading.sessions = false;
  }
}

async function toggleDisable(u) {
  await fetch(`/api/users/${encodeURIComponent(u.username)}`, {
    method: 'PATCH',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ disabled: !u.disabled })
  });
  await fetchUsers();
}

async function removeUser(u) {
  if (!confirm(`Eliminar ${u.username}?`)) return;
  await fetch(`/api/users/${encodeURIComponent(u.username)}`, { method: 'DELETE' });
  await fetchUsers();
}

async function clearRequests() {
  await fetch('/api/requests', { method: 'DELETE' });
  await fetchRequests();
}

async function selfTest() {
  await fetch('/test/radius', { method: 'POST' });
}

async function disconnectUser(u) {
  try {
    await fetch(`/api/users/${encodeURIComponent(u.username)}/disconnect`, { method: 'POST' });
    await Promise.all([fetchUsers(), fetchDevices()]);
  } catch {}
}

async function disconnectDevice(d) {
  try {
    await fetch(`/api/devices/${encodeURIComponent(d.id)}/disconnect`, { method: 'POST' });
    await Promise.all([fetchUsers(), fetchDevices()]);
  } catch {}
}

function setupSse() {
  const ev = new EventSource('/api/events');
  let refreshTimer = null;
  function scheduleRefresh() {
    if (refreshTimer) clearTimeout(refreshTimer);
    refreshTimer = setTimeout(async () => {
      await Promise.all([fetchUsers(), fetchDevices(), fetchSessions()]);
      refreshTimer = null;
    }, 3000); // Debounce de 3 segundos para evitar parpadeos
  }
  ev.addEventListener('message', (e) => {
    try {
      const data = JSON.parse(e.data);
      if (data && data.ts) requests.value.push(data);
      const t = data && data.type;
      if (t === 'authorize' || t === 'post-auth' || t === 'accounting' || t === 'coa-disconnect') {
        scheduleRefresh();
      }
    } catch {}
  });
  ev.addEventListener('clear', () => { requests.value = []; });
}

onMounted(async () => {
  try {
    const ue = JSON.parse(localStorage.getItem('ui_userExpanded') || '{}');
    Object.assign(userExpanded, ue && typeof ue === 'object' ? ue : {});
  } catch {}
  try {
    const de = JSON.parse(localStorage.getItem('ui_deviceExpanded') || '{}');
    Object.assign(deviceExpanded, de && typeof de === 'object' ? de : {});
  } catch {}
  await fetchUsers();
  await fetchDevices();
  await fetchRequests();
  await fetchSessions();
  setupSse();
  applyTheme();
  checkPWAStatus();
});

function checkPWAStatus() {
  if (localStorage.getItem('pwa_toast_dismissed')) return;
  const isStandalone = window.matchMedia('(display-mode: standalone)').matches || window.navigator.standalone === true;
  if (!isStandalone) {
    let deferredPrompt = null;
    window.addEventListener('beforeinstallprompt', (e) => {
      e.preventDefault();
      deferredPrompt = e;
      setTimeout(() => {
        toast.pwa('Instala RADIUS Nucleo como aplicación para una mejor experiencia', {
          title: '📱 Instalar Aplicación',
          position: 'top-center',
          duration: 0,
          persistent: true,
          action: {
            label: 'Instalar',
            handler: async () => {
              if (deferredPrompt) {
                deferredPrompt.prompt();
                const { outcome } = await deferredPrompt.userChoice;
                if (outcome === 'accepted') localStorage.setItem('pwa_toast_dismissed', 'true');
                deferredPrompt = null;
              }
            }
          }
        });
      }, 2000);
    });
  }
}

const filteredRequests = computed(() => {
  return requests.value.filter(ev => {
    if (eventFilters.type && ev.type !== eventFilters.type) return false;
    if (eventFilters.text) {
      const t = eventFilters.text.toLowerCase();
      const blob = JSON.stringify(ev).toLowerCase();
      if (!blob.includes(t)) return false;
    }
    return true;
  });
});

const filteredUsersAll = computed(() => {
  return users.value.filter(u => {
    if (userFilters.text && !u.username.toLowerCase().includes(userFilters.text.toLowerCase())) return false;
    if (userFilters.status === 'active' && u.disabled) return false;
    if (userFilters.status === 'disabled' && !u.disabled) return false;
    return true;
  });
});

const pageSize = 20;
const userPage = ref(0);
const filteredUsers = computed(() => filteredUsersAll.value.slice(userPage.value*pageSize, userPage.value*pageSize + pageSize));
watch([filteredUsersAll, () => layoutMode.value], () => { userPage.value = 0; });

const devicesById = computed(() => {
  const m = {};
  for (const d of devices.value) m[d.id] = d;
  return m;
});

function usersForDevice(id) {
  return users.value.filter(u => Array.isArray(u.dispositivos_utilizados) && u.dispositivos_utilizados.includes(id));
}

const devicesAll = computed(() => devices.value);
const devicePage = ref(0);
const pagedDevices = computed(() => devicesAll.value.slice(devicePage.value*pageSize, devicePage.value*pageSize + pageSize));
watch([devicesAll, () => layoutMode.value], () => { devicePage.value = 0; });

const sessionPage = ref(0);
const showActiveSessions = ref(true);
const showStaleSessions = ref(false);
const showStoppedSessions = ref(false);
const filteredSessions = computed(() => {
  return sessions.value.filter(s => {
    if (s.status === 'active' && !showActiveSessions.value) return false;
    if (s.status === 'stale' && !showStaleSessions.value) return false;
    if (s.status === 'stopped' && !showStoppedSessions.value) return false;
    return true;
  });
});
const pagedSessions = computed(() => filteredSessions.value.slice(sessionPage.value*pageSize, sessionPage.value*pageSize + pageSize));
watch([filteredSessions, () => layoutMode.value], () => { sessionPage.value = 0; });

const filteredRequestsAll = computed(() => filteredRequests.value);
const reqPage = ref(0);
const pagedRequests = computed(() => filteredRequestsAll.value.slice(reqPage.value*pageSize, reqPage.value*pageSize + pageSize));
watch(filteredRequestsAll, () => { reqPage.value = 0; });

watch(userExpanded, (v) => {
  try { localStorage.setItem('ui_userExpanded', JSON.stringify(v)); } catch {}
}, { deep: true });
watch(deviceExpanded, (v) => {
  try { localStorage.setItem('ui_deviceExpanded', JSON.stringify(v)); } catch {}
}, { deep: true });

function copyRequests() {
  const txt = JSON.stringify(requests.value, null, 2);
  navigator.clipboard?.writeText(txt);
}

function toggleTheme() {
  theme.value = theme.value === 'light' ? 'dark' : 'light';
  localStorage.setItem('theme', theme.value);
  applyTheme();
}
function applyTheme() {
  document.documentElement.classList.toggle('dark', theme.value === 'dark');
}

const showUserForm = ref(false);
const userFormMode = ref('create');
const userFormModel = ref({ username:'', password:'', vlan:'', disabled:false });

function openAddUser() {
  userFormMode.value = 'create';
  userFormModel.value = { username:'', password:'', vlan:'', disabled:false };
  showUserForm.value = true;
}
function openAddGuest() {
  userFormMode.value = 'guest';
  userFormModel.value = { username:'', password:'', vlan:'5', disabled:false, etiquetas: ['invitado'] };
  showUserForm.value = true;
}
const showRawDb = ref(false);
const rawDbFullscreen = ref(false);
function openRawDb() { showRawDb.value = true; }
function closeRawDb() { showRawDb.value = false; }
const showVlan = ref(false);
function openVlanForm() { showVlan.value = true; }
function closeVlan() { showVlan.value = false; }
function onVlanCreated() { showVlan.value = false; }
const showDevice = ref(false);
const deviceFormModel = ref({});
function openDeviceForm(d) { deviceFormModel.value = d; showDevice.value = true; }
function closeDevice() { showDevice.value = false; }
async function onDeviceSaved() { showDevice.value = false; await fetchDevices(); }

const showImport = ref(false);
const importKind = ref('users');
const importText = ref('');
const importFilename = ref('');
const importError = ref('');
const importFile = ref(null);
function openImport(kind){ importKind.value = kind; importText.value=''; showImport.value = true; }
function closeImport(){ showImport.value = false; }

async function submitImport(){
  importError.value = '';
  const txt = (importText.value || '').trim();
  if (!txt) { importError.value = 'El CSV está vacío.'; return; }
  const headers = getCsvHeaders(txt);
  if (!headers.length) { importError.value = 'No se encontraron columnas en el CSV.'; return; }
  const need = (k)=>headers.includes(k);
  if (importKind.value === 'users') {
    const missing = ['username','password'].filter(c=>!need(c));
    if (missing.length) { importError.value = `CSV de usuarios requiere columnas: username,password. Faltantes: ${missing.join(', ')}`; return; }
  } else if (importKind.value === 'devices') {
    const missing = ['mac'].filter(c=>!need(c));
    if (missing.length) { importError.value = `CSV de dispositivos requiere columna: mac.`; return; }
  } else if (importKind.value === 'vlans') {
    const missing = ['id'].filter(c=>!need(c));
    if (missing.length) { importError.value = `CSV de VLANs requiere columna: id.`; return; }
  }
  try {
    const ep = importKind.value === 'users' ? '/api/users/import' : importKind.value === 'devices' ? '/api/devices/import' : '/api/vlans/import';
    await fetch(ep, { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({ csv: importText.value }) });
    showImport.value = false;
    await Promise.all([fetchUsers(), fetchDevices()]);
  } catch {}
}

function onImportFile(e){
  const f = e.target.files && e.target.files[0];
  if (!f) return;
  importFilename.value = f.name;
  const reader = new FileReader();
  reader.onload = () => { importText.value = String(reader.result || ''); };
  reader.readAsText(f, 'utf-8');
  e.target.value = '';
}

function getCsvHeaders(text){
  const lines = text.split(/\r?\n/).filter(l=>l.trim().length>0);
  if (!lines.length) return [];
  const line = lines[0];
  const out=[]; let cur=''; let q=false;
  for (let i=0;i<line.length;i++){
    const ch=line[i];
    if (ch==='"'){
      if (q && line[i+1]==='"'){ cur+='"'; i++; }
      else { q=!q; }
    } else if (ch===',' && !q){ out.push(cur.trim().toLowerCase()); cur=''; }
    else { cur+=ch; }
  }
  out.push(cur.trim().toLowerCase());
  return out;
}

function openEditUser(u) {
  userFormMode.value = 'edit';
  userFormModel.value = { username: u.username, password: u.password || '', vlan: u.vlan || '', disabled: !!u.disabled };
  showUserForm.value = true;
}

async function handleUserFormSubmit(data) {
  if (userFormMode.value === 'edit') {
    await fetch(`/api/users/${encodeURIComponent(userFormModel.value.username)}`, {
      method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data)
    });
  } else {
    await fetch('/api/users', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) });
  }
  await fetchUsers();
  showUserForm.value = false;
}
</script>

<template>
  <!-- Top Bar -->
  <header class="sticky top-0 z-10 flex flex-wrap items-center gap-2.5 p-3 glass backdrop-blur-[14px] border-b border-pink-600/50 dark:border-pink-600/50 bg-gradient-to-r from-pink-600/20 via-transparent to-transparent">
    <div class="text-base font-bold tracking-wide flex-1">RADIUS Nucleo</div>
    <div class="flex flex-wrap items-center gap-2">
      <Button as="a" href="https://inicio.nucleoriofrio.com" variant="ghost">
        🏠 Inicio
      </Button>
      <Button variant="ghost" size="icon" @click="showInfoModal = true" title="Información del sistema">
        <svg class="size-4 opacity-90" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
          <circle cx="12" cy="12" r="10"></circle>
          <line x1="12" y1="16" x2="12" y2="12"></line>
          <line x1="12" y1="8" x2="12.01" y2="8"></line>
        </svg>
      </Button>
      <Button variant="ghost" size="icon" @click="toggleTheme">
        <img class="size-4 opacity-90" :src="theme === 'light' ? '/icons/moon.svg' : '/icons/sun.svg'" alt="theme">
      </Button>
      <Badge><span class="text-muted">Estado:</span> {{ statusText }}</Badge>
      <Button @click="openAddUser">
        <img class="size-4 opacity-90" src="/icons/user-plus.svg" alt="usuario"> Usuario
      </Button>
      <Button @click="openAddGuest">
        <img class="size-4 opacity-90" src="/icons/guest.svg" alt="invitado"> Invitado
      </Button>
      <UserDropdown />
      <DropdownMenu align="end">
        <template #trigger>
          <Button>
            <img class="size-4 opacity-90" src="/icons/settings.svg" alt="config"> Configuración
          </Button>
        </template>
        <DropdownMenuItem @click="openRawDb">ver rawDB</DropdownMenuItem>
        <DropdownMenuItem @click="openVlanForm">crear VLAN</DropdownMenuItem>
        <DropdownMenuSeparator />
        <DropdownMenuItem as="a" href="/api/users.csv">Exportar usuarios CSV</DropdownMenuItem>
        <DropdownMenuItem as="a" href="/api/devices.csv">Exportar dispositivos CSV</DropdownMenuItem>
        <DropdownMenuItem as="a" href="/api/vlans.csv">Exportar VLANs CSV</DropdownMenuItem>
        <DropdownMenuSeparator />
        <DropdownMenuItem @click="openImport('users')">Importar usuarios CSV</DropdownMenuItem>
        <DropdownMenuItem @click="openImport('devices')">Importar dispositivos CSV</DropdownMenuItem>
        <DropdownMenuItem @click="openImport('vlans')">Importar VLANs CSV</DropdownMenuItem>
      </DropdownMenu>
    </div>
  </header>

  <!-- Main Layout -->
  <section class="h-[calc(100vh-54px)] grid grid-cols-[360px_1fr] gap-3 p-3 max-md:grid-cols-1 max-md:grid-rows-[auto_1fr]"
    :class="{ 'grid-cols-[52px_1fr]': sidebarCollapsed && !mainCollapsed, 'grid-cols-[1fr_52px]': mainCollapsed && !sidebarCollapsed }">

    <!-- Sidebar: Eventos -->
    <aside>
      <Card variant="panel" class="h-full border-pink-200 dark:border-pink-600/50" :class="{ 'panel-collapsed': sidebarCollapsed }">
        <CardHeader>
          <CardTitle>Eventos FreeRADIUS</CardTitle>
          <CardActions>
            <Badge>Página {{ reqPage+1 }} / {{ Math.max(1, Math.ceil(filteredRequestsAll.length / pageSize)) }}</Badge>
            <Button size="sm" @click="reqPage=Math.max(0, reqPage-1)">Anterior</Button>
            <Button size="sm" @click="reqPage=Math.min(Math.ceil(filteredRequestsAll.length/pageSize)-1, reqPage+1)">Siguiente</Button>
            <Button size="sm" variant="ghost" title="Filtrar" @click="showEventFilters = true">
              <img class="size-4 opacity-90" src="/icons/filter.svg" alt="filtrar">
            </Button>
            <Button size="sm" variant="ghost" title="Limpiar" @click="clearRequests">
              <img class="size-4 opacity-90" src="/icons/clear.svg" alt="limpiar">
            </Button>
            <Button size="sm" variant="ghost" title="Test" @click="selfTest">
              <img class="size-4 opacity-90" src="/icons/test.svg" alt="test">
            </Button>
            <Button as="a" size="sm" variant="ghost" title="Descargar" href="/api/requests.csv" target="_blank">
              <img class="size-4 opacity-90" src="/icons/download.svg" alt="descargar">
            </Button>
            <Button size="sm" variant="ghost" title="Copiar" @click="copyRequests">
              <img class="size-4 opacity-90" src="/icons/copy.svg" alt="copiar">
            </Button>
            <Button size="sm" variant="ghost" class="hidden max-md:inline-flex" title="Colapsar" @click="sidebarCollapsed = !sidebarCollapsed">
              {{ sidebarCollapsed ? 'Expandir' : 'Colapsar' }}
            </Button>
          </CardActions>
        </CardHeader>
        <CardContent v-if="!sidebarCollapsed" class="flex-1 overflow-auto scroll-custom space-y-2">
          <div v-if="loading.requests" class="text-muted">Cargando eventos…</div>
          <EventCard v-for="ev in pagedRequests" :key="ev.id" :ev="ev" />
        </CardContent>
      </Card>
    </aside>

    <!-- Main: Usuarios -->
    <main>
      <Card variant="panel" class="h-full border-pink-200 dark:border-pink-600/50" :class="{ 'panel-collapsed': mainCollapsed }">
        <CardHeader>
          <div class="flex flex-wrap items-center gap-2 w-full">
            <CardTitle>Usuarios y Dispositivos</CardTitle>
            <span class="flex-1"></span>
            <Badge v-if="layoutMode==='user'">Página {{ userPage+1 }} / {{ Math.max(1, Math.ceil(filteredUsersAll.length / pageSize)) }}</Badge>
            <Badge v-else-if="layoutMode==='device'">Página {{ devicePage+1 }} / {{ Math.max(1, Math.ceil(devicesAll.length / pageSize)) }}</Badge>
            <Badge v-else>Página {{ sessionPage+1 }} / {{ Math.max(1, Math.ceil(filteredSessions.length / pageSize)) }}</Badge>
            <Button size="sm" @click="layoutMode==='user' ? (userPage=Math.max(0,userPage-1)) : layoutMode==='device' ? (devicePage=Math.max(0,devicePage-1)) : (sessionPage=Math.max(0,sessionPage-1))">Anterior</Button>
            <Button size="sm" @click="layoutMode==='user' ? (userPage=Math.min(Math.ceil(filteredUsersAll.length/pageSize)-1,userPage+1)) : layoutMode==='device' ? (devicePage=Math.min(Math.ceil(devicesAll.length/pageSize)-1,devicePage+1)) : (sessionPage=Math.min(Math.ceil(filteredSessions.length/pageSize)-1,sessionPage+1))">Siguiente</Button>
            <Button size="sm" :variant="layoutMode==='user' ? 'default' : 'ghost'" title="Vista usuarios" @click="layoutMode='user'">
              <img class="size-4 opacity-90" src="/icons/layout-users.svg" alt="usuarios"/> Usuarios
            </Button>
            <Button size="sm" :variant="layoutMode==='device' ? 'default' : 'ghost'" title="Vista dispositivos" @click="layoutMode='device'">
              <img class="size-4 opacity-90" src="/icons/layout-devices.svg" alt="dispositivos"/> Dispositivos
            </Button>
            <Button size="sm" :variant="layoutMode==='session' ? 'default' : 'ghost'" title="Vista sesiones activas" @click="layoutMode='session'; fetchSessions()">
              <svg class="size-4 opacity-90" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
                <path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path>
                <circle cx="9" cy="7" r="4"></circle>
                <path d="M23 21v-2a4 4 0 0 0-3-3.87"></path>
                <path d="M16 3.13a4 4 0 0 1 0 7.75"></path>
              </svg>
              Sesiones <Badge v-if="sessionStats.active > 0" variant="pink" class="ml-1 text-[10px] py-0">{{ sessionStats.active }}</Badge>
            </Button>
            <Button size="sm" variant="ghost" title="Filtrar" @click="showUserFilters = true">
              <img class="size-4 opacity-90" src="/icons/filter.svg" alt="filtro"/>
            </Button>
            <Button size="sm" variant="ghost" class="hidden max-md:inline-flex" title="Colapsar" @click="mainCollapsed = !mainCollapsed">
              {{ mainCollapsed ? 'Expandir' : 'Colapsar' }}
            </Button>
          </div>
        </CardHeader>
        <CardContent v-if="!mainCollapsed" class="flex-1 overflow-auto scroll-custom">
          <div v-if="loading.users" class="text-muted">Cargando usuarios…</div>
          <template v-else>
            <div v-if="layoutMode==='user'" class="grid grid-cols-[repeat(auto-fill,minmax(280px,1fr))] gap-2.5">
              <UserCard v-for="u in filteredUsers" :key="u.username" :item="u" :devicesById="devicesById"
                :expanded="!!userExpanded[u.username]"
                @toggle-expand="userExpanded[u.username] = !userExpanded[u.username]"
                @toggleDisable="toggleDisable" @remove="removeUser" @edit="openEditUser"
                @disconnect="disconnectUser" @edit-device="openDeviceForm" />
            </div>
            <div v-else-if="layoutMode==='device'" class="grid grid-cols-[repeat(auto-fill,minmax(280px,1fr))] gap-2.5">
              <DispositivoCard v-for="d in pagedDevices" :key="d.id" :device="d" :users="usersForDevice(d.id)" :devicesById="devicesById"
                :expanded="!!deviceExpanded[d.id]"
                @toggle-expand="deviceExpanded[d.id] = !deviceExpanded[d.id]"
                @edit="openDeviceForm" @disconnect="disconnectDevice" />
            </div>
            <!-- Session View -->
            <div v-else class="space-y-3">
              <!-- Stats -->
              <div class="flex flex-wrap gap-3 mb-4">
                <Badge
                  :variant="showActiveSessions ? 'pink' : 'secondary'"
                  class="text-sm py-1.5 px-3 cursor-pointer select-none transition-all"
                  :class="showActiveSessions ? 'ring-2 ring-pink-400/50' : 'opacity-60 hover:opacity-100'"
                  @click="showActiveSessions = !showActiveSessions"
                  :title="showActiveSessions ? 'Click para ocultar sesiones activas' : 'Click para mostrar sesiones activas'"
                >
                  <svg v-if="showActiveSessions" class="size-3.5 mr-1.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
                    <path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path>
                    <circle cx="12" cy="12" r="3"></circle>
                  </svg>
                  <svg v-else class="size-3.5 mr-1.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
                    <path d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24"></path>
                    <line x1="1" y1="1" x2="23" y2="23"></line>
                  </svg>
                  Activas: {{ sessionStats.active }}
                </Badge>
                <Badge
                  :variant="showStoppedSessions ? 'secondary' : 'secondary'"
                  class="text-sm py-1.5 px-3 cursor-pointer select-none transition-all"
                  :class="showStoppedSessions ? 'ring-2 ring-muted/50' : 'opacity-60 hover:opacity-100'"
                  @click="showStoppedSessions = !showStoppedSessions"
                  :title="showStoppedSessions ? 'Click para ocultar sesiones finalizadas' : 'Click para mostrar sesiones finalizadas'"
                >
                  <svg v-if="showStoppedSessions" class="size-3.5 mr-1.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
                    <path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path>
                    <circle cx="12" cy="12" r="3"></circle>
                  </svg>
                  <svg v-else class="size-3.5 mr-1.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
                    <path d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24"></path>
                    <line x1="1" y1="1" x2="23" y2="23"></line>
                  </svg>
                  Finalizadas: {{ sessionStats.stopped }}
                </Badge>
                <Badge
                  :variant="showStaleSessions ? 'warning' : 'secondary'"
                  class="text-sm py-1.5 px-3 cursor-pointer select-none transition-all"
                  :class="showStaleSessions ? 'ring-2 ring-yellow-400/50' : 'opacity-60 hover:opacity-100'"
                  @click="showStaleSessions = !showStaleSessions"
                  :title="showStaleSessions ? 'Click para ocultar sesiones stale' : 'Click para mostrar sesiones stale'"
                >
                  <svg v-if="showStaleSessions" class="size-3.5 mr-1.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
                    <path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path>
                    <circle cx="12" cy="12" r="3"></circle>
                  </svg>
                  <svg v-else class="size-3.5 mr-1.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
                    <path d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24"></path>
                    <line x1="1" y1="1" x2="23" y2="23"></line>
                  </svg>
                  Stale: {{ sessionStats.stale }}
                </Badge>
                <span class="flex-1"></span>
                <Button size="sm" @click="fetchSessions">
                  <svg class="size-4 mr-1" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
                    <path d="M21 12a9 9 0 1 1-9-9c2.52 0 4.93 1 6.74 2.74L21 8"></path>
                    <path d="M21 3v5h-5"></path>
                  </svg>
                  Actualizar
                </Button>
                <Button as="a" size="sm" variant="ghost" href="/api/sessions.csv" target="_blank">
                  <svg class="size-4 mr-1" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
                    <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
                    <polyline points="7 10 12 15 17 10"></polyline>
                    <line x1="12" y1="15" x2="12" y2="3"></line>
                  </svg>
                  Exportar CSV
                </Button>
              </div>
              <!-- Sessions Grid -->
              <div v-if="loading.sessions" class="text-muted">Cargando sesiones...</div>
              <div v-else-if="!filteredSessions.length" class="text-muted text-center py-8">
                <template v-if="!sessions.length">No hay sesiones</template>
                <template v-else>
                  No hay sesiones visibles
                  <span v-if="sessionStats.active && !showActiveSessions" class="block text-xs mt-1">{{ sessionStats.active }} activas ocultas</span>
                  <span v-if="sessionStats.stopped && !showStoppedSessions" class="block text-xs mt-1">{{ sessionStats.stopped }} finalizadas ocultas</span>
                  <span v-if="sessionStats.stale && !showStaleSessions" class="block text-xs mt-1">{{ sessionStats.stale }} stale ocultas</span>
                </template>
              </div>
              <div v-else class="grid grid-cols-[repeat(auto-fill,minmax(320px,1fr))] gap-3">
                <Card v-for="s in pagedSessions" :key="s.id" :class="['p-4', s.status === 'active' && 'border-pink-400/30 bg-pink-400/5']">
                  <div class="flex flex-wrap items-center gap-2 mb-3">
                    <Badge :variant="s.status === 'active' ? 'pink' : s.status === 'stale' ? 'warning' : 'secondary'">
                      {{ s.status }}
                    </Badge>
                    <span class="font-semibold">{{ s.username }}</span>
                    <span class="flex-1"></span>
                    <span class="text-xs text-muted">{{ new Date(s.started_at).toLocaleString('es-HN', { day: '2-digit', month: 'short', hour: '2-digit', minute: '2-digit' }) }}</span>
                  </div>
                  <div class="space-y-1.5 text-sm">
                    <div class="flex justify-between">
                      <span class="text-muted">MAC:</span>
                      <span class="font-mono text-xs">{{ s.mac || s.calling_station_id || '-' }}</span>
                    </div>
                    <div class="flex justify-between">
                      <span class="text-muted">Dispositivo:</span>
                      <span>{{ s.device_name || 'Sin nombre' }}</span>
                    </div>
                    <div class="flex justify-between">
                      <span class="text-muted">NAS:</span>
                      <span class="text-xs">{{ s.nas_id || s.nas_ip || '-' }}</span>
                    </div>
                    <div class="flex justify-between">
                      <span class="text-muted">Duración:</span>
                      <span>{{ s.session_time ? (Math.floor(s.session_time/3600) + 'h ' + Math.floor((s.session_time%3600)/60) + 'm') : '-' }}</span>
                    </div>
                    <div class="flex justify-between">
                      <span class="text-muted">Datos:</span>
                      <span>↓ {{ ((s.bytes_in || 0) / 1024 / 1024).toFixed(1) }} MB / ↑ {{ ((s.bytes_out || 0) / 1024 / 1024).toFixed(1) }} MB</span>
                    </div>
                  </div>
                </Card>
              </div>
            </div>
          </template>
        </CardContent>
      </Card>
    </main>
  </section>

  <!-- Modals -->
  <Modal :open="showEventFilters" title="Filtros de eventos" @close="showEventFilters=false">
    <div class="flex flex-wrap gap-2 my-2">
      <Input v-model="eventFilters.text" placeholder="Buscar texto" class="flex-1" />
      <select v-model="eventFilters.type" class="glass glass-border rounded-md px-3 py-2">
        <option value="">Todos</option>
        <option value="authorize">authorize</option>
        <option value="accounting">accounting</option>
        <option value="post-auth">post-auth</option>
        <option value="selftest">selftest</option>
        <option value="coa-disconnect">coa-disconnect</option>
      </select>
    </div>
  </Modal>

  <Modal :open="showUserFilters" title="Filtros de usuarios" @close="showUserFilters=false">
    <div class="flex flex-wrap gap-2 my-2">
      <Input v-model="userFilters.text" placeholder="Buscar usuario" class="flex-1" />
      <select v-model="userFilters.status" class="glass glass-border rounded-md px-3 py-2">
        <option value="">Todos</option>
        <option value="active">Activos</option>
        <option value="disabled">Deshabilitados</option>
      </select>
    </div>
  </Modal>

  <Modal :open="showUserForm" :title="userFormMode==='edit' ? 'Editar usuario' : (userFormMode==='guest' ? 'Agregar invitado' : 'Agregar usuario')"
    @close="showUserForm=false">
    <UserForm :model-value="userFormModel" :mode="userFormMode" @submit="handleUserFormSubmit" @cancel="showUserForm=false" />
  </Modal>

  <Modal :open="showRawDb" :fullscreen="rawDbFullscreen" title="Raw DB Viewer" @close="closeRawDb">
    <RawDbViewer @toggle-fullscreen="rawDbFullscreen = !rawDbFullscreen" />
  </Modal>

  <Modal :open="showVlan" title="Crear VLAN" @close="closeVlan">
    <VlanForm @success="onVlanCreated" @cancel="closeVlan" />
  </Modal>

  <Modal :open="showDevice" title="Editar dispositivo" @close="closeDevice">
    <DeviceForm :model="deviceFormModel" @success="onDeviceSaved" @cancel="closeDevice" />
  </Modal>

  <Modal :open="showImport" :title="'Importar ' + importKind + ' CSV'" @close="closeImport">
    <div class="flex flex-col gap-2">
      <div class="flex flex-wrap items-center gap-2">
        <input ref="importFile" type="file" accept=".csv,text/csv" @change="onImportFile" class="hidden" />
        <Button @click="importFile?.click()">Seleccionar archivo CSV</Button>
        <span v-if="importFilename" class="text-muted text-sm">{{ importFilename }}</span>
      </div>
      <Textarea
        v-model="importText"
        :rows="10"
        placeholder="Pega CSV aquí o selecciona un archivo"
      />
      <div v-if="importError" class="text-sm text-red-400">{{ importError }}</div>
      <div class="flex justify-end gap-2 mt-2">
        <Button variant="ghost" @click="closeImport">Cancelar</Button>
        <Button @click="submitImport">Importar</Button>
      </div>
    </div>
  </Modal>

  <!-- Info Modal -->
  <Modal :open="showInfoModal" title="Información del Sistema RADIUS" @close="showInfoModal = false">
    <div class="space-y-6 max-h-[70vh] overflow-y-auto scroll-custom pr-2">
      <!-- Sesiones -->
      <section>
        <h3 class="text-lg font-semibold mb-2 flex items-center gap-2">
          <svg class="size-5 text-pink-400" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
            <circle cx="12" cy="12" r="10"></circle>
            <polyline points="12 6 12 12 16 14"></polyline>
          </svg>
          Sesiones
        </h3>
        <div class="text-sm text-muted space-y-2">
          <p>Una <strong class="text-foreground">sesión</strong> representa el período de tiempo durante el cual un dispositivo está conectado a la red WiFi.</p>
          <p><strong class="text-foreground">Estados de sesión:</strong></p>
          <ul class="list-disc list-inside space-y-1 ml-2">
            <li><Badge variant="pink" class="text-[10px]">active</Badge> — Dispositivo actualmente conectado y enviando datos</li>
            <li><Badge variant="warning" class="text-[10px]">stale</Badge> — Sin actividad por más de 10 minutos (posible desconexión sin notificar)</li>
            <li><Badge variant="secondary" class="text-[10px]">stopped</Badge> — Sesión finalizada correctamente</li>
          </ul>
          <p class="mt-2"><strong class="text-foreground">Información disponible:</strong> Usuario, dispositivo (MAC), hora de inicio/fin, duración, datos transferidos (bytes enviados/recibidos).</p>
          <p><strong class="text-foreground">Limitación:</strong> Si un dispositivo se desconecta abruptamente (batería, pérdida de señal), puede quedar como "stale" hasta que el sistema lo detecte.</p>
        </div>
      </section>

      <!-- Eventos FreeRADIUS -->
      <section>
        <h3 class="text-lg font-semibold mb-2 flex items-center gap-2">
          <svg class="size-5 text-pink-400" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
            <path d="M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5L14.5 2z"></path>
            <polyline points="14 2 14 8 20 8"></polyline>
          </svg>
          Eventos FreeRADIUS
        </h3>
        <div class="text-sm text-muted space-y-2">
          <p>El servidor RADIUS recibe eventos del Access Point (NAS) en tiempo real:</p>
          <ul class="list-disc list-inside space-y-1.5 ml-2">
            <li><strong class="text-foreground">authorize</strong> — Cuando un dispositivo intenta conectarse. Se verifica usuario/contraseña y se decide si permitir acceso.</li>
            <li><strong class="text-foreground">post-auth</strong> — Después de autenticación exitosa. Se asigna VLAN y se registra el dispositivo.</li>
            <li><strong class="text-foreground">accounting</strong> — Durante la sesión:
              <ul class="list-disc list-inside ml-4 mt-1 space-y-0.5">
                <li><em>Start</em> — Inicio de sesión</li>
                <li><em>Interim-Update</em> — Actualización periódica con estadísticas</li>
                <li><em>Stop</em> — Fin de sesión (desconexión)</li>
              </ul>
            </li>
            <li><strong class="text-foreground">coa-disconnect</strong> — Desconexión forzada desde la interfaz (Change of Authorization).</li>
          </ul>
        </div>
      </section>

      <!-- Usuarios -->
      <section>
        <h3 class="text-lg font-semibold mb-2 flex items-center gap-2">
          <svg class="size-5 text-pink-400" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
            <path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"></path>
            <circle cx="12" cy="7" r="4"></circle>
          </svg>
          Usuarios
        </h3>
        <div class="text-sm text-muted space-y-2">
          <p>Los <strong class="text-foreground">usuarios normales</strong> son cuentas permanentes para acceso a la red:</p>
          <ul class="list-disc list-inside space-y-1 ml-2">
            <li>Tienen usuario y contraseña únicos</li>
            <li>Se les asigna una VLAN específica</li>
            <li>Pueden conectar múltiples dispositivos</li>
            <li>Pueden ser deshabilitados temporalmente sin eliminar la cuenta</li>
          </ul>
        </div>
      </section>

      <!-- Invitados -->
      <section>
        <h3 class="text-lg font-semibold mb-2 flex items-center gap-2">
          <svg class="size-5 text-pink-400" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
            <path d="M16 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path>
            <circle cx="8.5" cy="7" r="4"></circle>
            <line x1="20" y1="8" x2="20" y2="14"></line>
            <line x1="23" y1="11" x2="17" y2="11"></line>
          </svg>
          Invitados
        </h3>
        <div class="text-sm text-muted space-y-2">
          <p>Los <strong class="text-foreground">invitados</strong> son usuarios temporales para visitantes:</p>
          <ul class="list-disc list-inside space-y-1 ml-2">
            <li>Creados con un solo clic mediante el botón "Invitado"</li>
            <li>Se asignan automáticamente a la VLAN de invitados (VLAN 5)</li>
            <li>Tienen la etiqueta <Badge variant="secondary" class="text-[10px]">invitado</Badge></li>
            <li>El usuario se genera aleatoriamente (ej: invitado_a3x9)</li>
            <li><strong class="text-foreground">Auto-deshabilitación:</strong> Si se configura, el usuario se deshabilita automáticamente después de cierto tiempo</li>
          </ul>
        </div>
      </section>

      <!-- Dispositivos -->
      <section>
        <h3 class="text-lg font-semibold mb-2 flex items-center gap-2">
          <svg class="size-5 text-pink-400" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
            <rect x="5" y="2" width="14" height="20" rx="2" ry="2"></rect>
            <line x1="12" y1="18" x2="12.01" y2="18"></line>
          </svg>
          Dispositivos
        </h3>
        <div class="text-sm text-muted space-y-2">
          <p>Los <strong class="text-foreground">dispositivos</strong> son los equipos físicos que se conectan (teléfonos, laptops, etc.):</p>
          <ul class="list-disc list-inside space-y-1 ml-2">
            <li>Se identifican por su dirección <strong class="text-foreground">MAC</strong> (única para cada dispositivo)</li>
            <li>Se registran automáticamente cuando un usuario se conecta</li>
            <li>Pueden tener un nombre descriptivo (ej: "iPhone de Juan")</li>
            <li>Un dispositivo puede pertenecer a múltiples usuarios (compartido)</li>
            <li>Se puede desconectar un dispositivo específico sin afectar otros del mismo usuario</li>
          </ul>
          <p class="mt-2"><strong class="text-foreground">Relación Usuario-Dispositivo:</strong></p>
          <ul class="list-disc list-inside space-y-1 ml-2">
            <li><em>dispositivos_utilizados</em> — Todos los dispositivos que ha usado el usuario</li>
            <li><em>dispositivos_conectados</em> — Dispositivos actualmente conectados</li>
          </ul>
        </div>
      </section>

      <!-- Flujo de conexión -->
      <section>
        <h3 class="text-lg font-semibold mb-2 flex items-center gap-2">
          <svg class="size-5 text-pink-400" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
            <polyline points="22 12 18 12 15 21 9 3 6 12 2 12"></polyline>
          </svg>
          Flujo de Conexión
        </h3>
        <div class="text-sm text-muted space-y-2">
          <ol class="list-decimal list-inside space-y-1.5 ml-2">
            <li>Dispositivo intenta conectarse al WiFi con usuario/contraseña</li>
            <li>Access Point (NAS) envía solicitud <em>authorize</em> al servidor RADIUS</li>
            <li>RADIUS verifica credenciales y responde Accept/Reject</li>
            <li>Si es Accept, NAS envía <em>post-auth</em> y luego <em>accounting Start</em></li>
            <li>Durante la conexión, NAS envía <em>Interim-Updates</em> periódicos</li>
            <li>Al desconectar, NAS envía <em>accounting Stop</em></li>
          </ol>
        </div>
      </section>
    </div>
  </Modal>

  <!-- Toast System -->
  <Toast position="top-center" />
</template>

<style>
/* Panel collapsed state */
.panel-collapsed .scroll-custom,
.panel-collapsed [class*="CardContent"] {
  display: none;
}
</style>
