/* ════════════════════════════════
   INFINITY — BACKEND
   Express + sesiones + JSON storage
   ════════════════════════════════ */

const express     = require('express');
const cookieParser = require('cookie-parser');
const fs          = require('fs').promises;
const fsSync      = require('fs');
const path        = require('path');
const crypto      = require('crypto');
const multer      = require('multer');
const sharp       = require('sharp');
const XLSX        = require('xlsx');
const PDFDocument = require('pdfkit');
const archiver    = require('archiver');

const app    = express();
const PORT   = process.env.PORT || 3000;
const SECRET = process.env.SESSION_SECRET || 'infinity-cambia-esta-clave-en-produccion';

const DATA_DIR     = path.join(__dirname, 'data');
const FRONTEND_DIR = path.join(__dirname, 'frontend');
const IMG_DIR      = path.join(__dirname, 'Imagenes');
const PEDIDOS_DIR  = path.join(__dirname, 'pedidos');

// ════════════════════════════════
// MIDDLEWARES
// ════════════════════════════════
app.use(express.json());
app.use(cookieParser(SECRET));
app.use(express.static(FRONTEND_DIR));
app.use('/Imagenes', express.static(IMG_DIR, { maxAge: '7d' }));
app.use('/pedidos', express.static(PEDIDOS_DIR));

// Evita el 404 de favicon.ico
app.get('/favicon.ico', (req, res) => res.status(204).end());

// ════════════════════════════════
// HELPERS
// ════════════════════════════════
async function readJson(filename) {
  const filePath = path.join(DATA_DIR, filename);
  const data = await fs.readFile(filePath, 'utf8');
  return JSON.parse(data);
}

async function writeJson(filename, data) {
  const filePath = path.join(DATA_DIR, filename);
  await fs.writeFile(filePath, JSON.stringify(data, null, 2) + '\n', 'utf8');
}

async function ensureSesiones() {
  try {
    await fs.access(path.join(DATA_DIR, 'sesiones.json'));
  } catch {
    await writeJson('sesiones.json', []);
  }
}

async function getSesiones() {
  await ensureSesiones();
  return await readJson('sesiones.json');
}

async function saveSesiones(sesiones) {
  await writeJson('sesiones.json', sesiones);
}

async function createSession(usuario) {
  const sesiones = await getSesiones();
  const token = crypto.randomBytes(32).toString('hex');
  sesiones.push({
    token,
    usuario: usuario.usuario,
    rol:    usuario.rol,
    createdAt: Date.now()
  });
  await saveSesiones(sesiones);
  return token;
}

async function destroySession(token) {
  const sesiones = await getSesiones();
  const filtradas = sesiones.filter(s => s.token !== token);
  await saveSesiones(filtradas);
}

async function getSessionUser(req) {
  const token = req.signedCookies.session;
  if (!token) return null;
  const sesiones = await getSesiones();
  return sesiones.find(s => s.token === token) || null;
}

async function getUserData(usuario) {
  try {
    const data = await readJson('contraseñas.json');
    return (data.usuarios || []).find(u => u.usuario === usuario) || null;
  } catch {
    return null;
  }
}

// ════════════════════════════════
// HELPERS — Categorías
// ════════════════════════════════
const SIN_CATEGORIA_ID = 'sin-categoria';

function slugify(text) {
  return String(text)
    .toLowerCase()
    .normalize('NFD').replace(/[\u0300-\u036f]/g, '') // quitar acentos
    .replace(/[^a-z0-9\s-]/g, '')                     // quitar chars especiales
    .trim()
    .replace(/\s+/g, '-')                              // espacios → guiones
    .replace(/-+/g, '-')                               // colapsar guiones
    .replace(/^-+|-+$/g, '');                          // trim guiones
}

async function getCategorias() {
  try {
    return await readJson('categorias.json');
  } catch {
    return [];
  }
}

async function saveCategorias(categorias) {
  await writeJson('categorias.json', categorias);
}

// ════════════════════════════════
// TREE HELPERS — para subcategorías anidadas
// Estructura: cada categoría puede tener un array opcional `subcategorias`
// con la misma forma, recursivamente. Máximo 3 niveles (cat > sub > sub-sub).
// ════════════════════════════════
function findCategoryById(categorias, id) {
  for (const c of categorias) {
    if (c.id === id) return c;
    if (Array.isArray(c.subcategorias) && c.subcategorias.length) {
      const found = findInSubcats(c.subcategorias, id);
      if (found) return found;
    }
  }
  return null;
}

function findInSubcats(subcats, id) {
  for (const s of subcats) {
    if (s.id === id) return s;
    if (Array.isArray(s.subcategorias) && s.subcategorias.length) {
      const found = findInSubcats(s.subcategorias, id);
      if (found) return found;
    }
  }
  return null;
}

// Devuelve la ruta desde la raíz hasta el nodo con ese id.
// Ej: para "morrales" en bolsos>hombre>morrales → [{id:bolsos}, {id:hombre}, {id:morrales}]
function getCategoryPath(categorias, id, trail = []) {
  for (const c of categorias) {
    const next = [...trail, c];
    if (c.id === id) return next;
    if (Array.isArray(c.subcategorias) && c.subcategorias.length) {
      const found = getCategoryPath(c.subcategorias, id, next);
      if (found) return found;
    }
  }
  return null;
}

// Recolecta todos los ids de subcategorías dentro de un array de subcats.
// Usado para validar duplicados al crear una nueva subcategoría.
function collectSubcatIds(subcats, acc = new Set()) {
  if (!Array.isArray(subcats)) return acc;
  for (const s of subcats) {
    if (s.id) acc.add(s.id);
    if (Array.isArray(s.subcategorias)) collectSubcatIds(s.subcategorias, acc);
  }
  return acc;
}

// Quita una subcategoría del árbol (modifica in place). Devuelve true si la encontró.
function removeSubcatFromTree(categorias, id) {
  for (const c of categorias) {
    if (Array.isArray(c.subcategorias)) {
      const idx = c.subcategorias.findIndex(s => s.id === id);
      if (idx !== -1) {
        c.subcategorias.splice(idx, 1);
        return true;
      }
      if (removeSubcatFromTree(c.subcategorias, id)) return true;
    }
  }
  return false;
}

// Busca una subcat por id en un array de subcats (cualquier nivel) y la devuelve, o null.
function findSubcatRecursive(subcats, id) {
  if (!Array.isArray(subcats)) return null;
  for (const s of subcats) {
    if (s.id === id) return s;
    if (Array.isArray(s.subcategorias)) {
      const found = findSubcatRecursive(s.subcategorias, id);
      if (found) return found;
    }
  }
  return null;
}

// Quita una subcat de un array de subcats (cualquier nivel) y la devuelve. O null si no está.
function removeSubcatRecursive(subcats, id) {
  if (!Array.isArray(subcats)) return null;
  for (let i = 0; i < subcats.length; i++) {
    if (subcats[i].id === id) {
      const [removed] = subcats.splice(i, 1);
      return removed;
    }
    if (Array.isArray(subcats[i].subcategorias)) {
      const found = removeSubcatRecursive(subcats[i].subcategorias, id);
      if (found) return found;
    }
  }
  return null;
}

async function ensureSinCategoria() {
  const categorias = await getCategorias();
  if (!categorias.some(c => c.id === SIN_CATEGORIA_ID)) {
    categorias.push({ id: SIN_CATEGORIA_ID, nombre: 'Sin categoría', icono: '📦' });
    await saveCategorias(categorias);
  }
  // Asegurar carpeta física
  const dir = path.join(IMG_DIR, SIN_CATEGORIA_ID);
  try {
    await fs.access(dir);
  } catch {
    await fs.mkdir(dir, { recursive: true });
  }
}

async function getProductos() {
  try {
    return await readJson('productos.json');
  } catch {
    return [];
  }
}

async function saveProductos(productos) {
  await writeJson('productos.json', productos);
}

// Helper: mover la imagen de un producto entre carpetas.
// - oldImg: ruta actual relativa a /Imagenes/ (ej: "bolsos/AF100-124.webp")
// - newCat: id de la categoría destino (o "agotados")
// - ref:   referencia del producto (la imagen destino será "<ref>.webp")
// Devuelve la nueva ruta relativa ("<newCat>/<ref>.webp").
// Si la imagen origen no existe, no falla: igual devuelve la nueva ruta
// (útil para entradas huérfanas en el JSON).
async function moveProductImage(oldImg, newCat, ref) {
  const newName = `${ref}.webp`;
  const newPath = path.join(IMG_DIR, newCat, newName);

  // Crear carpeta destino si no existe
  await fs.mkdir(path.join(IMG_DIR, newCat), { recursive: true });

  if (oldImg) {
    const oldPath = path.join(IMG_DIR, oldImg);
    try {
      await fs.rename(oldPath, newPath);
    } catch (err) {
      if (err.code === 'ENOENT') {
        // Imagen origen no existe: no es error (entrada huérfana).
        // Igual devolvemos la nueva ruta; el JSON quedará coherente.
      } else {
        // Cross-device u otro error: copiar y borrar.
        try {
          await fs.copyFile(oldPath, newPath);
          await fs.unlink(oldPath);
        } catch (err2) {
          if (err2.code !== 'ENOENT') throw err2;
        }
      }
    }
  }

  return `${newCat}/${newName}`;
}

// Helper: borrar la imagen de un producto (best-effort).
async function deleteProductImage(img) {
  if (!img) return;
  const imgPath = path.join(IMG_DIR, img);
  try {
    await fs.unlink(imgPath);
  } catch (err) {
    if (err.code !== 'ENOENT') throw err;
  }
}

// ════════════════════════════════
// MIDDLEWARES DE AUTENTICACIÓN
// ════════════════════════════════
async function requireAuth(req, res, next) {
  const session = await getSessionUser(req);
  if (!session) return res.status(401).json({ error: 'No autorizado' });
  req.user = session;
  next();
}

async function requireAdmin(req, res, next) {
  const session = await getSessionUser(req);
  if (!session) return res.status(401).json({ error: 'No autorizado' });

  const user = await getUserData(session.usuario);
  if (!user) return res.status(401).json({ error: 'No autorizado' });

  if (user.rol !== 'admin') {
    return res.status(403).json({ error: 'Se requiere rol de administrador' });
  }
  req.user = user;
  next();
}

// Permite acceder si es admin O tiene el permiso 'usuarios'
async function requireUserManagement(req, res, next) {
  const session = await getSessionUser(req);
  if (!session) return res.status(401).json({ error: 'No autorizado' });

  const user = await getUserData(session.usuario);
  if (!user) return res.status(401).json({ error: 'No autorizado' });

  const esAdmin = user.rol === 'admin';
  const tienePermiso = (user.permisos || []).includes('usuarios');

  if (!esAdmin && !tienePermiso) {
    return res.status(403).json({ error: 'No tienes permiso para gestionar usuarios' });
  }
  req.user = user;
  next();
}

// Permite acceder si es admin O tiene el permiso 'categorias'
async function requireCategoriasManagement(req, res, next) {
  const session = await getSessionUser(req);
  if (!session) return res.status(401).json({ error: 'No autorizado' });

  const user = await getUserData(session.usuario);
  if (!user) return res.status(401).json({ error: 'No autorizado' });

  const esAdmin = user.rol === 'admin';
  const tienePermiso = (user.permisos || []).includes('categorias');

  if (!esAdmin && !tienePermiso) {
    return res.status(403).json({ error: 'No tienes permiso para gestionar categorías' });
  }
  req.user = user;
  next();
}

// Permite acceder si es admin O tiene el permiso 'productos'
async function requireProductosManagement(req, res, next) {
  const session = await getSessionUser(req);
  if (!session) return res.status(401).json({ error: 'No autorizado' });

  const user = await getUserData(session.usuario);
  if (!user) return res.status(401).json({ error: 'No autorizado' });

  const esAdmin = user.rol === 'admin';
  const tienePermiso = (user.permisos || []).includes('productos');

  if (!esAdmin && !tienePermiso) {
    return res.status(403).json({ error: 'No tienes permiso para gestionar productos' });
  }
  req.user = user;
  next();
}

// Permite acceder si es admin O tiene el permiso 'descargas'
async function requireDescargasAccess(req, res, next) {
  const session = await getSessionUser(req);
  if (!session) return res.status(401).json({ error: 'No autorizado' });

  const user = await getUserData(session.usuario);
  if (!user) return res.status(401).json({ error: 'No autorizado' });

  const esAdmin = user.rol === 'admin';
  const tienePermiso = (user.permisos || []).includes('descargas');

  if (!esAdmin && !tienePermiso) {
    return res.status(403).json({ error: 'No tienes permiso para acceder a Descargas' });
  }
  req.user = user;
  next();
}

// ════════════════════════════════
// RUTAS DE AUTENTICACIÓN
// ════════════════════════════════
app.post('/api/auth/login', async (req, res) => {
  const { usuario, contraseña } = req.body || {};
  if (!usuario || !contraseña) {
    return res.status(400).json({ error: 'Usuario y contraseña requeridos' });
  }

  try {
    const data     = await readJson('contraseñas.json');
    const usuarios = data.usuarios || [];
    const match    = usuarios.find(u => u.usuario === usuario && u.contraseña === contraseña);

    if (!match) return res.status(401).json({ error: 'Credenciales inválidas' });

    const token = await createSession(match);
    res.cookie('session', token, {
      signed:    true,
      httpOnly:  true,
      sameSite:  'lax',
      secure:    false,   // ⚠️ cambiar a true cuando se use HTTPS
      maxAge:    24 * 60 * 60 * 1000
    });
    res.json({ ok: true, usuario: match.usuario, rol: match.rol });
  } catch (err) {
    console.error('Login error:', err);
    res.status(500).json({ error: 'Error en el servidor' });
  }
});

app.post('/api/auth/logout', async (req, res) => {
  const token = req.signedCookies.session;
  if (token) await destroySession(token);
  res.clearCookie('session');
  res.json({ ok: true });
});

app.get('/api/auth/me', async (req, res) => {
  const session = await getSessionUser(req);
  if (!session) return res.status(401).json({ error: 'No autenticado' });

  const user = await getUserData(session.usuario);
  if (!user) return res.status(401).json({ error: 'Usuario no encontrado' });

  res.json({
    usuario:  user.usuario,
    rol:      user.rol,
    permisos: user.permisos || []
  });
});

app.put('/api/auth/password', requireAdmin, async (req, res) => {
  const { actual, nueva } = req.body || {};
  if (!actual || !nueva) {
    return res.status(400).json({ error: 'Contraseña actual y nueva requeridas' });
  }
  if (nueva.length < 6) {
    return res.status(400).json({ error: 'La nueva contraseña debe tener al menos 6 caracteres' });
  }

  try {
    const data     = await readJson('contraseñas.json');
    const usuarios = data.usuarios || [];
    const idx      = usuarios.findIndex(u => u.usuario === req.user.usuario);

    if (idx === -1) return res.status(404).json({ error: 'Usuario no encontrado' });
    if (usuarios[idx].contraseña !== actual) {
      return res.status(401).json({ error: 'La contraseña actual es incorrecta' });
    }

    usuarios[idx].contraseña = nueva;
    await writeJson('contraseñas.json', { usuarios });
    res.json({ ok: true });
  } catch (err) {
    console.error('Change password error:', err);
    res.status(500).json({ error: 'Error al cambiar contraseña' });
  }
});

// ════════════════════════════════
// RUTAS PÚBLICAS (catálogo)
// ════════════════════════════════
app.get('/api/categorias', async (req, res) => {
  try {
    await ensureSinCategoria();
    const categorias = await getCategorias();
    res.json(categorias);
  } catch {
    res.json([]);
  }
});

app.get('/api/productos', async (req, res) => {
  try {
    const data = await readJson('productos.json');
    res.json(data);
  } catch {
    res.json([]);
  }
});

// ════════════════════════════════
// GESTIÓN DE CATEGORÍAS (admin o permiso 'categorias')
// ════════════════════════════════
app.post('/api/categorias', requireCategoriasManagement, async (req, res) => {
  const { nombre, icono } = req.body || {};

  if (!nombre || !nombre.trim()) {
    return res.status(400).json({ error: 'El nombre es obligatorio' });
  }

  const id = slugify(nombre);
  if (!id) {
    return res.status(400).json({ error: 'El nombre genera un ID inválido' });
  }

  try {
    const categorias = await getCategorias();
    const nombreNorm = nombre.trim().toLowerCase();

    // No permitir duplicados por id NI por nombre
    if (categorias.some(c => c.id === id)) {
      return res.status(409).json({ error: 'Ya existe una categoría con ese nombre' });
    }
    if (categorias.some(c => c.nombre.toLowerCase() === nombreNorm)) {
      return res.status(409).json({ error: 'Ya existe una categoría con ese nombre' });
    }

    // Crear carpeta física
    const dir = path.join(IMG_DIR, id);
    try {
      await fs.access(dir);
      // Si ya existe la carpeta, no es error — solo se agrega al JSON
    } catch {
      await fs.mkdir(dir, { recursive: true });
    }

    const nuevaCat = { id, nombre: nombre.trim(), icono: icono || '📦' };
    categorias.push(nuevaCat);
    await saveCategorias(categorias);

    res.json({ ok: true, categoria: nuevaCat });
  } catch (err) {
    console.error('Create category error:', err);
    res.status(500).json({ error: 'Error al crear la categoría' });
  }
});

app.put('/api/categorias/:id', requireCategoriasManagement, async (req, res) => {
  const targetId = req.params.id;
  const { nombre, icono } = req.body || {};

  if (!nombre || !nombre.trim()) {
    return res.status(400).json({ error: 'El nombre es obligatorio' });
  }

  try {
    const categorias = await getCategorias();
    const idx = categorias.findIndex(c => c.id === targetId);

    if (idx === -1) return res.status(404).json({ error: 'Categoría no encontrada' });

    if (targetId === SIN_CATEGORIA_ID) {
      return res.status(400).json({ error: 'No se puede editar la categoría del sistema' });
    }

    const nombreNorm = nombre.trim().toLowerCase();
    // No permitir que el nuevo nombre coincida con OTRA categoría
    if (categorias.some((c, i) => i !== idx && c.nombre.toLowerCase() === nombreNorm)) {
      return res.status(409).json({ error: 'Ya existe otra categoría con ese nombre' });
    }

    // El id NO se modifica (es inmutable). Solo nombre e icono.
    categorias[idx].nombre = nombre.trim();
    if (icono) categorias[idx].icono = icono;

    await saveCategorias(categorias);
    res.json({ ok: true, categoria: categorias[idx] });
  } catch (err) {
    console.error('Update category error:', err);
    res.status(500).json({ error: 'Error al actualizar la categoría' });
  }
});

app.delete('/api/categorias/:id', requireCategoriasManagement, async (req, res) => {
  const targetId = req.params.id;

  if (targetId === SIN_CATEGORIA_ID) {
    return res.status(400).json({ error: 'No se puede eliminar la categoría del sistema' });
  }

  try {
    const categorias = await getCategorias();
    const idx = categorias.findIndex(c => c.id === targetId);

    if (idx === -1) return res.status(404).json({ error: 'Categoría no encontrada' });

    // Asegurar "sin-categoria" existe
    await ensureSinCategoria();

    // Contar productos en esta categoría
    const productos = await getProductos();
    const itemsAfectados = productos.filter(p => p.cat === targetId);

    if (itemsAfectados.length > 0) {
      // Mover imágenes de Imagenes/<targetId>/ → Imagenes/sin-categoria/
      const srcDir = path.join(IMG_DIR, targetId);
      const dstDir = path.join(IMG_DIR, SIN_CATEGORIA_ID);

      try {
        const files = await fs.readdir(srcDir);
        for (const file of files) {
          const srcFile = path.join(srcDir, file);
          const dstFile = path.join(dstDir, file);
          // Mover (rename). Si existe destino, sobreescribir (cada producto tiene img único).
          try {
            await fs.rename(srcFile, dstFile);
          } catch (err) {
            // Si falla el rename (p.ej. cross-device), copiar y borrar
            await fs.copyFile(srcFile, dstFile);
            await fs.unlink(srcFile);
          }
        }
        // Borrar carpeta origen (vacía)
        try {
          await fs.rmdir(srcDir);
        } catch {
          // Si quedan subcarpetas u otros archivos, no fallar
        }
      } catch (err) {
        // Si la carpeta no existe, no es error
        if (err.code !== 'ENOENT') throw err;
      }

      // Actualizar productos: cat → sin-categoria, img → sin-categoria/...
      for (const p of productos) {
        if (p.cat === targetId) {
          p.cat = SIN_CATEGORIA_ID;
          if (p.img && p.img.startsWith(targetId + '/')) {
            p.img = SIN_CATEGORIA_ID + '/' + p.img.slice(targetId.length + 1);
          }
        }
      }
      await saveProductos(productos);
    } else {
      // Sin items: solo borrar carpeta y entrada del JSON
      const srcDir = path.join(IMG_DIR, targetId);
      try {
        const files = await fs.readdir(srcDir);
        for (const file of files) {
          await fs.unlink(path.join(srcDir, file));
        }
        await fs.rmdir(srcDir);
      } catch (err) {
        if (err.code !== 'ENOENT') console.error('Error borrando carpeta:', err);
      }
    }

    // Quitar del JSON
    categorias.splice(idx, 1);
    await saveCategorias(categorias);

    res.json({ ok: true, itemsMovidos: itemsAfectados.length });
  } catch (err) {
    console.error('Delete category error:', err);
    res.status(500).json({ error: 'Error al eliminar la categoría' });
  }
});

// ════════════════════════════════
// SUBCATEGORÍAS (anidadas, máx 3 niveles)
// CRUD separado del de categorías para validaciones más finas.
// No afecta productos todavía (esa integración viene en el siguiente paso).
// ════════════════════════════════

// POST /api/categorias/:id/subcategorias  →  crear subcat dentro de :id
// Body: { nombre, parentSubId? }
//   - Sin parentSubId: crea subcat nivel 1 (directo dentro de la categoría)
//   - Con parentSubId: crea sub-subcat (nivel 2) dentro de la subcat indicada
app.post('/api/categorias/:id/subcategorias', requireCategoriasManagement, async (req, res) => {
  const parentId = req.params.id;
  const { nombre, parentSubId } = req.body || {};

  if (!nombre || !String(nombre).trim() || String(nombre).length > 40) {
    return res.status(400).json({ error: 'Nombre inválido (1-40 caracteres)' });
  }
  if (parentId === SIN_CATEGORIA_ID) {
    return res.status(400).json({ error: 'No se pueden agregar subcategorías a "Sin categoría"' });
  }

  try {
    const categorias = await getCategorias();
    const parent = categorias.find(c => c.id === parentId);
    if (!parent) return res.status(404).json({ error: 'Categoría padre no encontrada' });

    // Determinar el array destino: subcategorías de la categoría, o de la sub-sub
    let targetArray;
    if (parentSubId) {
      // Creando sub-sub: el "padre" es una subcat existente
      const subParent = Array.isArray(parent.subcategorias)
        ? parent.subcategorias.find(s => s.id === parentSubId)
        : null;
      if (!subParent) return res.status(404).json({ error: 'Subcategoría padre no encontrada' });
      if (!Array.isArray(subParent.subcategorias)) subParent.subcategorias = [];
      targetArray = subParent.subcategorias;
    } else {
      // Creando subcat nivel 1
      if (!Array.isArray(parent.subcategorias)) parent.subcategorias = [];
      targetArray = parent.subcategorias;
    }

    // Límite de profundidad: máximo 3 niveles (cat > sub > sub-sub).
    // Si estamos creando nivel 1 y la categoría ya tiene sub-subcats (de otras ramas),
    // todavía está OK (cada rama puede tener sub-sub independientes).
    // Lo que NO permitimos es crear un nivel 3 (porque la URL no lo soporta y la UI no lo ofrece).

    // Generar id (slug) y validar duplicado dentro del padre directo
    const slug = slugify(nombre);
    if (!slug) return res.status(400).json({ error: 'Nombre inválido' });

    const idsHermanos = new Set(targetArray.map(s => s.id));
    if (idsHermanos.has(slug)) {
      return res.status(409).json({ error: `Ya existe una subcategoría "${slug}" aquí` });
    }

    targetArray.push({ id: slug, nombre: String(nombre).trim() });
    await saveCategorias(categorias);

    res.json({ ok: true, subcategoria: { id: slug, nombre: String(nombre).trim() } });
  } catch (err) {
    console.error('Create subcategory error:', err);
    res.status(500).json({ error: 'Error al crear subcategoría' });
  }
});

// PUT /api/categorias/:id/subcategorias/:subId  →  editar nombre (id inmutable)
// Busca recursivamente en el árbol: :id es la categoría raíz, :subId puede estar en cualquier nivel.
app.put('/api/categorias/:id/subcategorias/:subId', requireCategoriasManagement, async (req, res) => {
  const { id: parentId, subId } = req.params;
  const { nombre } = req.body || {};

  if (!nombre || !String(nombre).trim() || String(nombre).length > 40) {
    return res.status(400).json({ error: 'Nombre inválido (1-40 caracteres)' });
  }

  try {
    const categorias = await getCategorias();
    const parent = categorias.find(c => c.id === parentId);
    if (!parent) return res.status(404).json({ error: 'Categoría padre no encontrada' });

    const sub = findSubcatRecursive(parent.subcategorias, subId);
    if (!sub) return res.status(404).json({ error: 'Subcategoría no encontrada' });

    sub.nombre = String(nombre).trim();
    await saveCategorias(categorias);
    res.json({ ok: true, subcategoria: { id: sub.id, nombre: sub.nombre } });
  } catch (err) {
    console.error('Edit subcategory error:', err);
    res.status(500).json({ error: 'Error al editar subcategoría' });
  }
});

// DELETE /api/categorias/:id/subcategorias/:subId  →  eliminar (mover productos al padre)
// Solo se permite borrar hojas (subcats sin sub-subcats). Si tiene sub-subcats, debe borrarlas primero.
app.delete('/api/categorias/:id/subcategorias/:subId', requireCategoriasManagement, async (req, res) => {
  const { id: parentId, subId } = req.params;

  try {
    const categorias = await getCategorias();
    const parent = categorias.find(c => c.id === parentId);
    if (!parent) return res.status(404).json({ error: 'Categoría padre no encontrada' });

    const sub = findSubcatRecursive(parent.subcategorias, subId);
    if (!sub) return res.status(404).json({ error: 'Subcategoría no encontrada' });

    if (Array.isArray(sub.subcategorias) && sub.subcategorias.length) {
      return res.status(400).json({ error: 'Primero elimina las sub-subcategorías' });
    }

    // Quitar del árbol (recursivo, encuentra el padre correcto y splice)
    removeSubcatRecursive(parent.subcategorias, subId);
    // Mantener array limpio: si la raíz quedó sin subcats, eliminamos la propiedad
    if (Array.isArray(parent.subcategorias) && parent.subcategorias.length === 0) {
      delete parent.subcategorias;
    }

    await saveCategorias(categorias);
    res.json({ ok: true });
  } catch (err) {
    console.error('Delete subcategory error:', err);
    res.status(500).json({ error: 'Error al eliminar subcategoría' });
  }
});

// ════════════════════════════════
// GESTIÓN DE USUARIOS (admin o permiso 'usuarios')
// ════════════════════════════════
app.get('/api/usuarios', requireUserManagement, async (req, res) => {
  try {
    const data     = await readJson('contraseñas.json');
    const usuarios = (data.usuarios || []).map(u => ({
      usuario:  u.usuario,
      rol:      u.rol,
      permisos: u.permisos || []
    }));
    res.json(usuarios);
  } catch (err) {
    console.error('List users error:', err);
    res.status(500).json({ error: 'Error al cargar usuarios' });
  }
});

app.post('/api/usuarios', requireUserManagement, async (req, res) => {
  const { usuario, contraseña, rol, permisos } = req.body || {};

  if (!usuario || !contraseña || !rol) {
    return res.status(400).json({ error: 'Todos los campos son obligatorios' });
  }
  if (contraseña.length < 6) {
    return res.status(400).json({ error: 'La contraseña debe tener al menos 6 caracteres' });
  }
  if (!['admin', 'editor', 'vendedor'].includes(rol)) {
    return res.status(400).json({ error: 'Rol inválido' });
  }

  try {
    const data     = await readJson('contraseñas.json');
    const usuarios = data.usuarios || [];

    if (usuarios.some(u => u.usuario === usuario)) {
      return res.status(409).json({ error: 'Ya existe un usuario con ese nombre' });
    }

    // Admin siempre tiene todos los permisos; editor/vendedor reciben la lista enviada
    const permisosFinal = rol === 'admin'
      ? []
      : (Array.isArray(permisos) ? permisos : []);

    usuarios.push({ usuario, contraseña, rol, permisos: permisosFinal });
    await writeJson('contraseñas.json', { usuarios });
    res.json({ ok: true });
  } catch (err) {
    console.error('Create user error:', err);
    res.status(500).json({ error: 'Error al crear usuario' });
  }
});

app.put('/api/usuarios/:usuario', requireUserManagement, async (req, res) => {
  const targetUsuario = req.params.usuario;
  const { contraseña, rol, permisos } = req.body || {};

  if (rol && !['admin', 'editor', 'vendedor'].includes(rol)) {
    return res.status(400).json({ error: 'Rol inválido' });
  }
  if (contraseña && contraseña.length < 6) {
    return res.status(400).json({ error: 'La contraseña debe tener al menos 6 caracteres' });
  }

  try {
    const data     = await readJson('contraseñas.json');
    const usuarios = data.usuarios || [];
    const idx      = usuarios.findIndex(u => u.usuario === targetUsuario);

    if (idx === -1) return res.status(404).json({ error: 'Usuario no encontrado' });

    if (rol && rol !== 'admin' && usuarios[idx].rol === 'admin') {
      const admins = usuarios.filter(u => u.rol === 'admin');
      if (admins.length === 1) {
        return res.status(400).json({ error: 'No puedes degradar al único administrador' });
      }
    }

    if (contraseña) usuarios[idx].contraseña = contraseña;
    if (rol)        usuarios[idx].rol = rol;

    // Si cambia a admin, limpiamos permisos (admin siempre tiene todos)
    if (rol === 'admin') {
      usuarios[idx].permisos = [];
    } else if (Array.isArray(permisos)) {
      usuarios[idx].permisos = permisos;
    }

    await writeJson('contraseñas.json', { usuarios });
    res.json({ ok: true });
  } catch (err) {
    console.error('Update user error:', err);
    res.status(500).json({ error: 'Error al actualizar usuario' });
  }
});

app.delete('/api/usuarios/:usuario', requireUserManagement, async (req, res) => {
  const targetUsuario = req.params.usuario;

  if (targetUsuario === req.user.usuario) {
    return res.status(400).json({ error: 'No puedes eliminarte a ti mismo' });
  }

  try {
    const data     = await readJson('contraseñas.json');
    const usuarios = data.usuarios || [];
    const target   = usuarios.find(u => u.usuario === targetUsuario);

    if (!target) return res.status(404).json({ error: 'Usuario no encontrado' });

    if (target.rol === 'admin') {
      const admins = usuarios.filter(u => u.rol === 'admin');
      if (admins.length === 1) {
        return res.status(400).json({ error: 'No puedes eliminar al único administrador' });
      }
    }

    const filtrados = usuarios.filter(u => u.usuario !== targetUsuario);
    await writeJson('contraseñas.json', { usuarios: filtrados });
    res.json({ ok: true });
  } catch (err) {
    console.error('Delete user error:', err);
    res.status(500).json({ error: 'Error al eliminar usuario' });
  }
});

// ════════════════════════════════
// SUBIDA MASIVA DE PRODUCTOS (admin o permiso 'productos')
// ════════════════════════════════

// Multer config: usa memoria (luego sharp lo procesa y guarda en disco).
const MAX_FILES  = 50;
const MAX_SIZE   = 20 * 1024 * 1024; // 20MB por archivo
const MAX_WIDTH   = 1200;              // ancho (o alto) máx del webp resultante
const WEBP_QUALITY = 80;

const upload = multer({
  storage: multer.memoryStorage(),
  limits: { fileSize: MAX_SIZE, files: MAX_FILES }
});

// Convención de nombres de archivo:
//   La REFERENCIA es TODO el nombre antes de la extensión, sin importar
//   qué tan complejo sea (puede tener guiones, guiones bajos, números, etc.).
//   Cada archivo = 1 producto con 1 foto.
//
//   "bill-001-1.jpg"   → ref=bill-001-1
//   "bill-001-2.jpg"   → ref=bill-001-2   (producto DISTINTO al anterior)
//   "GG-500505.jpg"    → ref=GG-500505
//   "mi-prod_loco.jpg" → ref=mi-prod_loco
//
// La referencia es el IDENTIFICATIVO del producto (como un SKU).
// Más adelante, cuando se suba el Excel/CSV, se matcheará por referencia
// para asignar la descripción (nombre visible) y el precio.
const REF_VALID_RE = /^[a-zA-Z0-9]+(?:[-_a-zA-Z0-9]*[a-zA-Z0-9])?$/;

function parseFilename(name) {
  // Quitar extensión (todo lo que está después del último punto)
  const base = name.replace(/\.[^.]+$/, '').trim();
  if (!base) return null;

  // Validar que la referencia tenga un formato aceptable
  if (!REF_VALID_RE.test(base)) return null;

  return { ref: base, num: 1 };
}

app.post('/api/productos/upload', requireProductosManagement, upload.array('fotos', MAX_FILES), async (req, res) => {
  const categoria = (req.body.categoria || '').trim();
  const subRaw    = req.body.sub || '[]';
  const files    = req.files || [];

  if (!categoria) {
    return res.status(400).json({ error: 'Selecciona una categoría' });
  }
  if (files.length === 0) {
    return res.status(400).json({ error: 'No se enviaron fotos' });
  }

  // Parsear la cadena de subcategorías (viene como JSON string desde el form data)
  let subPath;
  try {
    subPath = JSON.parse(subRaw);
    if (!Array.isArray(subPath)) throw new Error();
  } catch {
    return res.status(400).json({ error: 'Subcategoría inválida' });
  }

  try {
    // Verificar que la categoría existe y NO es sin-categoria
    const categorias = await getCategorias();
    const cat = categorias.find(c => c.id === categoria);
    if (!cat) {
      return res.status(400).json({ error: 'La categoría no existe' });
    }
    if (categoria === SIN_CATEGORIA_ID) {
      return res.status(400).json({ error: 'No se puede subir productos a la categoría del sistema' });
    }

    // Validar la cadena de subcats contra el árbol:
    // - La categoría tiene subcats → subPath DEBE tener al menos 1 elemento (la hoja final)
    // - La categoría NO tiene subcats → subPath debe estar vacío
    // - Cada sub debe existir en su nivel y el último debe ser HOJA (sin sub-subs)
    const catTieneSubcats = Array.isArray(cat.subcategorias) && cat.subcategorias.length > 0;

    if (catTieneSubcats && subPath.length === 0) {
      return res.status(400).json({ error: `"${cat.nombre}" tiene subcategorías, debes elegir una hoja` });
    }
    if (!catTieneSubcats && subPath.length > 0) {
      return res.status(400).json({ error: `"${cat.nombre}" no tiene subcategorías` });
    }

    // Caminar el árbol validando cada nivel
    let nivelActual = cat.subcategorias || [];
    for (let i = 0; i < subPath.length; i++) {
      const id = subPath[i];
      const nodo = nivelActual.find(s => s.id === id);
      if (!nodo) {
        return res.status(400).json({ error: `Subcategoría "${id}" no encontrada en nivel ${i + 1}` });
      }
      const esUltimo = i === subPath.length - 1;
      if (esUltimo) {
        // La hoja final NO debe tener sub-subcategorías (solo las hojas reciben productos)
        if (Array.isArray(nodo.subcategorias) && nodo.subcategorias.length > 0) {
          return res.status(400).json({ error: `"${nodo.nombre}" tiene sub-subcategorías, debes elegir una de ellas` });
        }
      } else {
        // No es el último → debe tener subcategorías para seguir bajando
        if (!Array.isArray(nodo.subcategorias) || nodo.subcategorias.length === 0) {
          return res.status(400).json({ error: `"${nodo.nombre}" no tiene sub-subcategorías` });
        }
        nivelActual = nodo.subcategorias;
      }
    }

    // Construir el path destino: <cat> o <cat>/<sub1> o <cat>/<sub1>/<sub2>
    const pathParts = [categoria, ...subPath];
    const relDir    = pathParts.join('/');
    const dstDir    = path.join(IMG_DIR, relDir);
    await fs.mkdir(dstDir, { recursive: true });

    // Cargar productos existentes
    const productos = await getProductos();
    const existentes = new Set(productos.map(p => p.ref || p.id));

    const creados   = [];
    const saltados  = [];
    const ignorados = [];
    const errores   = [];

    // Cada archivo = 1 producto con 1 foto. Procesamos secuencialmente.
    for (const f of files) {
      const parsed = parseFilename(f.originalname);
      if (!parsed) {
        ignorados.push({ archivo: f.originalname, motivo: 'Nombre de archivo inválido' });
        continue;
      }

      const { ref } = parsed;

      if (existentes.has(ref)) {
        saltados.push({ ref, motivo: 'Ya existe un producto con esa referencia' });
        continue;
      }

      try {
        const outName = `${ref}.webp`;
        const outPath = path.join(dstDir, outName);

        await sharp(f.buffer)
          .rotate()                                 // respeta EXIF orientation
          .resize({
            width:  MAX_WIDTH,
            height: MAX_WIDTH,
            fit:    'inside',
            withoutEnlargement: true
          })
          .webp({ quality: WEBP_QUALITY })
          .toFile(outPath);

        productos.push({
          id:   ref,
          ref:  ref,
          cat:  categoria,
          sub:  subPath,
          img:  `${relDir}/${outName}`
        });
        creados.push({ ref });
      } catch (err) {
        console.error(`Error procesando ${ref}:`, err.message);
        errores.push({ ref, motivo: 'Error al procesar la imagen' });
      }
    }

    await saveProductos(productos);

    res.json({
      ok: true,
      resumen: {
        creados:   creados.length,
        creadosList:   creados,
        saltados:  saltados.length,
        saltadosList:  saltados,
        ignorados: ignorados.length,
        ignoradosList: ignorados,
        errores:   errores.length,
        erroresList:   errores
      }
    });
  } catch (err) {
    console.error('Upload productos error:', err);
    res.status(500).json({ error: 'Error al procesar la subida' });
  }
});

// Marcar como AGOTADO (admin o permiso 'productos')
// Mueve la imagen a Imagenes/agotados/<ref>.webp y actualiza el JSON.
// El producto sigue en el JSON (con su categoría original), pero su `img`
// pasa a apuntar a "agotados/<ref>.webp", que es la señal de "agotado".
app.post('/api/productos/:ref/agotar', requireProductosManagement, async (req, res) => {
  const ref = req.params.ref;
  try {
    const productos = await getProductos();
    const idx = productos.findIndex(p => p.ref === ref);
    if (idx === -1) return res.status(404).json({ error: 'Producto no encontrado' });

    if (productos[idx].img && productos[idx].img.startsWith('agotados/')) {
      return res.status(400).json({ error: 'El producto ya está agotado' });
    }

    const oldImg = productos[idx].img;
    const newImg = await moveProductImage(oldImg, 'agotados', ref);
    productos[idx].img = newImg;
    await saveProductos(productos);

    res.json({ ok: true, ref, img: newImg });
  } catch (err) {
    console.error('Agotar error:', err);
    res.status(500).json({ error: 'Error al marcar como agotado' });
  }
});

// REACTIVAR un producto agotado (admin o permiso 'productos')
// Mueve la imagen de vuelta a su carpeta de categoría original
// (o a "sin-categoria" si la categoría ya no existe) y actualiza el JSON.
app.post('/api/productos/:ref/reactivar', requireProductosManagement, async (req, res) => {
  const ref = req.params.ref;
  try {
    const productos = await getProductos();
    const idx = productos.findIndex(p => p.ref === ref);
    if (idx === -1) return res.status(404).json({ error: 'Producto no encontrado' });

    if (!productos[idx].img || !productos[idx].img.startsWith('agotados/')) {
      return res.status(400).json({ error: 'El producto no está agotado' });
    }

    // Categoría original (la guardamos en el JSON al subir)
    const targetCat = productos[idx].cat;
    if (!targetCat) {
      return res.status(400).json({ error: 'El producto no tiene categoría original' });
    }

    // Si la categoría original ya no existe, fallback a "sin-categoria"
    const categorias = await getCategorias();
    let destCat = targetCat;
    if (!categorias.some(c => c.id === targetCat)) {
      await ensureSinCategoria();
      destCat = SIN_CATEGORIA_ID;
      productos[idx].cat = SIN_CATEGORIA_ID;
    }

    const newImg = await moveProductImage(productos[idx].img, destCat, ref);
    productos[idx].img = newImg;
    await saveProductos(productos);

    res.json({ ok: true, ref, img: newImg, cat: destCat });
  } catch (err) {
    console.error('Reactivar error:', err);
    res.status(500).json({ error: 'Error al reactivar el producto' });
  }
});

// ELIMINAR DEFINITIVAMENTE un producto (admin o permiso 'productos')
// Borra la imagen del disco y la entrada del JSON.
// Funciona tanto para productos disponibles como para agotados.
app.delete('/api/productos/:ref', requireProductosManagement, async (req, res) => {
  const ref = req.params.ref;
  try {
    const productos = await getProductos();
    const idx = productos.findIndex(p => p.ref === ref);
    if (idx === -1) return res.status(404).json({ error: 'Producto no encontrado' });

    const img = productos[idx].img;
    await deleteProductImage(img);

    productos.splice(idx, 1);
    await saveProductos(productos);

    res.json({ ok: true, ref });
  } catch (err) {
    console.error('Delete producto error:', err);
    res.status(500).json({ error: 'Error al eliminar el producto' });
  }
});

// ════════════════════════════════
// ASIGNAR NOMBRE + PRECIO DESDE EXCEL/CSV (admin o permiso 'productos')
//
// El archivo debe tener encabezados en la primera fila:
//   A: REFERENCIA | B: DESCRIPCION | C: PRECIO
//
// Reglas:
//   - El Excel/CSV NO crea productos nuevos. Solo actualiza los que ya
//     existen en productos.json (match por `ref` exacto).
//   - Si una celda está vacía, NO sobrescribe el valor actual.
//   - Precio acepta: "50000", "$ 50.000", "50,000.00", "50.000,50", etc.
//   - Si un `ref` del archivo no existe, se reporta en "noEncontrados".
// ════════════════════════════════

// Helper: normalizar texto para comparar headers (trim, lowercase, sin acentos)
function normHeader(s) {
  return String(s || '')
    .trim()
    .toLowerCase()
    .normalize('NFD').replace(/[\u0300-\u036f]/g, '');
}

// Helper: parsear precio en múltiples formatos (CO/US/EU)
//   "50000"       → 50000
//   "$ 50.000"    → 50000   (formato CO: punto = miles)
//   "50,000.00"   → 50000   (formato US: coma = miles, punto = decimal)
//   "50.000,50"   → 50000.5 (formato CO/EU: punto = miles, coma = decimal)
//   "  $1.234,56" → 1234.56
//   "75.500"      → 75500   (heurística: 3 dígitos tras último punto = miles)
//   "75.5"        → 75.5    (1-2 dígitos tras último punto = decimal)
function parsePrice(value) {
  if (value === null || value === undefined || value === '') return null;
  let s = String(value).trim();
  if (!s) return null;

  // Quitar símbolos de moneda y espacios
  s = s.replace(/[$€£¥₹\s]/g, '');
  if (!s) return null;

  const isNegative = (s.startsWith('-') || (s.startsWith('(') && s.endsWith(')')));
  s = s.replace(/^[-()]/, '').replace(/\)$/, '');

  const lastComma = s.lastIndexOf(',');
  const lastDot   = s.lastIndexOf('.');

  let normalized;
  if (lastComma !== -1 && lastDot !== -1) {
    // Hay ambos separadores: el ÚLTIMO es el decimal
    if (lastComma > lastDot) {
      // Formato CO/EU: "1.000,50" → coma es decimal
      normalized = s.replace(/\./g, '').replace(',', '.');
    } else {
      // Formato US: "1,000.50" → punto es decimal
      normalized = s.replace(/,/g, '');
    }
  } else if (lastComma !== -1) {
    // Solo comas. Si la última parte tiene 1-2 dígitos, decimal; si no, miles
    const parts = s.split(',');
    if (parts.length === 2 && parts[1].length <= 2) {
      normalized = s.replace(',', '.');
    } else {
      normalized = s.replace(/,/g, '');
    }
  } else if (lastDot !== -1) {
    // Solo puntos. Heurística: si la parte tras el último punto tiene 3 dígitos
    // (y no hay otro punto), es separador de miles (formato CO: "75.000" = 75000).
    // Si tiene 1-2 dígitos, es decimal ("75.5" = 75.5).
    const afterLastDot = s.slice(lastDot + 1);
    if (s.split('.').length === 2 && afterLastDot.length === 3 && /^\d+$/.test(afterLastDot)) {
      // Formato CO sin comas: "75.000" = 75000
      normalized = s.replace(/\./g, '');
    } else {
      // Decimal: "75.5", "75.50", "1.5.5" (raro pero lo dejamos pasar)
      normalized = s;
    }
  } else {
    normalized = s;
  }

  const num = parseFloat(normalized);
  if (isNaN(num)) return null;
  return isNegative ? -num : num;
}

// Multer específico para el Excel (1 solo archivo, hasta 5MB)
const excelUpload = multer({
  storage: multer.memoryStorage(),
  limits: { fileSize: 5 * 1024 * 1024, files: 1 }
});

app.post('/api/productos/excel', requireProductosManagement, excelUpload.single('archivo'), async (req, res) => {
  const file = req.file;
  if (!file) {
    return res.status(400).json({ error: 'No se envió ningún archivo' });
  }

  try {
    // Parsear el archivo (.xlsx o .csv). SheetJS detecta el formato por contenido.
    // codepage: 65001 fuerza UTF-8 (necesario para acentos/ñ en CSV).
    const wb = XLSX.read(file.buffer, { type: 'buffer', codepage: 65001 });
    const sheetName = wb.SheetNames[0];
    if (!sheetName) {
      return res.status(400).json({ error: 'El archivo no tiene hojas' });
    }
    const sheet = wb.Sheets[sheetName];
    // header: 1 → array de arrays. raw: false devuelve el valor FORMATEADO de la celda
    // (preserva strings tal cual aparecen, ej: "45.000,50" se queda como string).
    // Esto es crítico para no interpretar mal separadores de miles/decimales.
    // defval: '' para celdas vacías.
    const rows = XLSX.utils.sheet_to_json(sheet, { header: 1, defval: '', raw: false });

    if (rows.length === 0) {
      return res.status(400).json({ error: 'El archivo está vacío' });
    }

    // Validar encabezados (case-insensitive, sin acentos)
    const headersRaw = rows[0].map(h => normHeader(h));
    const expected = ['referencia', 'descripcion', 'precio'];
    if (
      headersRaw.length < 3 ||
      headersRaw[0] !== expected[0] ||
      headersRaw[1] !== expected[1] ||
      headersRaw[2] !== expected[2]
    ) {
      return res.status(400).json({
        error: `Los encabezados deben ser exactamente: ${expected.join(' | ').toUpperCase()}. Encontrado: ${rows[0].map(h => String(h).trim() || '(vacío)').join(' | ').toUpperCase()}`
      });
    }

    // Procesar filas
    const productos     = await getProductos();
    const productosByRef = new Map(productos.map(p => [String(p.ref), p]));

    const actualizados   = [];
    const noEncontrados  = [];
    const errores        = [];
    const filasVacias    = [];
    let filasConDatos    = 0;

    for (let i = 1; i < rows.length; i++) {
      const row = rows[i];

      // Saltar filas completamente vacías
      const isEmpty = row.every(c => c === '' || c === null || c === undefined);
      if (isEmpty) {
        filasVacias.push(i + 1); // 1-indexed para el usuario
        continue;
      }

      const refRaw       = row[0] != null ? String(row[0]).trim() : '';
      const descRaw      = row[1] != null ? String(row[1]).trim() : '';
      const precioRaw    = row[2];

      if (!refRaw) {
        errores.push({ fila: i + 1, motivo: 'REFERENCIA vacía' });
        continue;
      }

      const producto = productosByRef.get(refRaw);
      if (!producto) {
        noEncontrados.push(refRaw);
        continue;
      }

      // Actualizar SOLO si la celda tiene valor
      let changed = false;
      if (descRaw) {
        producto.nombre = descRaw;
        changed = true;
      }
      if (precioRaw !== '' && precioRaw !== null && precioRaw !== undefined) {
        // XLSX puede entregar números o strings según formato de celda
        let precio;
        if (typeof precioRaw === 'number') {
          precio = precioRaw;
        } else {
          precio = parsePrice(precioRaw);
        }
        if (precio === null || isNaN(precio)) {
          errores.push({ fila: i + 1, ref: refRaw, motivo: `Precio inválido: "${precioRaw}"` });
          continue;
        }
        producto.precio = precio;
        changed = true;
      }

      if (changed) {
        actualizados.push({ ref: refRaw, fila: i + 1 });
        filasConDatos++;
      }
    }

    await saveProductos(productos);

    res.json({
      ok: true,
      resumen: {
        actualizados: actualizados.length,
        actualizadosList: actualizados,
        noEncontrados: noEncontrados.length,
        noEncontradosList: noEncontrados,
        errores: errores.length,
        erroresList: errores,
        filasVacias: filasVacias.length,
        filasVaciasList: filasVacias
      }
    });
  } catch (err) {
    console.error('Excel upload error:', err);
    res.status(500).json({ error: 'Error al procesar el archivo: ' + err.message });
  }
});

// Multer error handler (tamaño excedido, etc.)
app.use((err, req, res, next) => {
  if (err instanceof multer.MulterError) {
    if (err.code === 'LIMIT_FILE_SIZE') {
      return res.status(400).json({ error: `Cada archivo no puede superar ${MAX_SIZE / 1024 / 1024}MB` });
    }
    if (err.code === 'LIMIT_FILE_COUNT') {
      return res.status(400).json({ error: `Máximo ${MAX_FILES} archivos por subida` });
    }
    if (err.code === 'LIMIT_UNEXPECTED_FILE') {
      return res.status(400).json({ error: 'Campo de archivo inesperado' });
    }
    return res.status(400).json({ error: err.message });
  }
  next(err);
});

// ════════════════════════════════
// FALLBACK para SPA-like del admin
// ════════════════════════════════
app.get('/administrativo', (req, res) => {
  res.sendFile(path.join(FRONTEND_DIR, 'administrativo', 'index.html'));
});
app.get('/administrativo/', (req, res) => {
  res.sendFile(path.join(FRONTEND_DIR, 'administrativo', 'index.html'));
});

// ════════════════════════════════
// LIMPIEZA DE SESIONES A LAS 00:00 (America/Bogota)
// Vacía sesiones.json todos los días a medianoche hora Colombia.
// Cuando los usuarios abran la app después de las 00:00, tendrán
// que volver a loguearse (sus cookies quedaron firmadas pero el
// token ya no existe en sesiones.json → 401 → redirige a /login).
// ════════════════════════════════
function msUntilNextMidnightBogota() {
  // Obtener la hora actual en America/Bogota (UTC-5, sin DST)
  const formatter = new Intl.DateTimeFormat('en-US', {
    timeZone: 'America/Bogota',
    hour12: false,
    hour:   '2-digit',
    minute: '2-digit',
    second: '2-digit'
  });
  const parts = {};
  for (const part of formatter.formatToParts(new Date())) {
    if (part.type !== 'literal') parts[part.type] = part.value;
  }
  // En formato 24h, medianoche puede aparecer como "24" en algunos sistemas
  const hour   = parseInt(parts.hour === '24' ? '0' : parts.hour, 10);
  const minute = parseInt(parts.minute, 10);
  const second = parseInt(parts.second, 10);
  const msPassedToday = (hour * 3600 + minute * 60 + second) * 1000;
  return 24 * 3600 * 1000 - msPassedToday;
}

async function runSessionCleanup(logPrefix = '[cleanup]') {
  try {
    const sesiones = await getSesiones();
    if (sesiones.length === 0) {
      console.log(`${logPrefix} sesiones.json ya está vacío, nada que limpiar`);
      return { removed: 0 };
    }
    await saveSesiones([]);
    console.log(`${logPrefix} ✅ ${sesiones.length} sesión(es) cerrada(s) a las 00:00 CO`);
    return { removed: sesiones.length };
  } catch (err) {
    console.error(`${logPrefix} Error limpiando sesiones:`, err.message);
    return { removed: 0, error: err.message };
  }
}

function scheduleSessionCleanup() {
  const ms = msUntilNextMidnightBogota();
  const hours = (ms / 1000 / 60 / 60).toFixed(2);
  console.log(`[cleanup] Próxima limpieza de sesiones en ${hours}h (00:00 America/Bogota)`);

  setTimeout(() => {
    runSessionCleanup();
    // Después de la primera ejecución, repetir cada 24h
    setInterval(() => runSessionCleanup(), 24 * 60 * 60 * 1000);
  }, ms);
}

// ════════════════════════════════
// ENDPOINT MANUAL (admin) — útil para emergencias o testing
// Cierra TODAS las sesiones inmediatamente.
// ════════════════════════════════
app.post('/api/admin/cleanup-sessions', requireAdmin, async (req, res) => {
  const result = await runSessionCleanup('[manual]');
  res.json({ ok: true, ...result });
});

// ════════════════════════════════
// GENERACIÓN DE PDF DEL CATÁLOGO (admin o permiso 'descargas')
//
// Body: {
//   seleccion: [{ cat: 'bolsos', sub: ['dama', 'aaa'] }, ...]
// }
//   - cat: id de la categoría raíz
//   - sub: array opcional con la cadena de subcategorías (puede ser [] para "toda la categoría")
//
// Layout del PDF (A4 vertical):
//   1. Portada con logo, título "Catálogo" y fecha
//   2. Una página (o más) por cada hoja seleccionada, con:
//      - Subtítulo grande con la ruta completa (Cat > Sub1 > Sub2)
//      - Grid 2xN de productos: foto + ref + nombre + precio
//   3. Footer con número de página en todas las hojas
// ════════════════════════════════
app.post('/api/descargas/pdf', requireDescargasAccess, async (req, res) => {
  const { seleccion } = req.body || {};

  if (!Array.isArray(seleccion) || seleccion.length === 0) {
    return res.status(400).json({ error: 'Debes seleccionar al menos una categoría o subcategoría' });
  }

  try {
    const categorias = await getCategorias();
    const productos  = await getProductos();

    // Validar que cada item de selección existe en el árbol
    for (const item of seleccion) {
      if (!item || typeof item.cat !== 'string') {
        return res.status(400).json({ error: 'Selección inválida' });
      }
      const cat = categorias.find(c => c.id === item.cat);
      if (!cat) {
        return res.status(400).json({ error: `Categoría "${item.cat}" no existe` });
      }
      if (Array.isArray(item.sub) && item.sub.length > 0) {
        let nodo = cat;
        for (let i = 0; i < item.sub.length; i++) {
          const subs = Array.isArray(nodo.subcategorias) ? nodo.subcategorias : [];
          const sig = subs.find(s => s.id === item.sub[i]);
          if (!sig) {
            return res.status(400).json({ error: `Subcategoría "${item.sub[i]}" no encontrada` });
          }
          nodo = sig;
        }
      }
    }

    // Filtrar productos que coincidan con alguna selección.
    // - Excluye sin-categoria y agotados
    // - Excluye productos sin imagen
    const productosFiltrados = productos.filter(p => {
      if (!p.cat || p.cat === SIN_CATEGORIA_ID) return false;
      if (!p.img) return false;
      if (p.img.startsWith('agotados/')) return false;

      return seleccion.some(sel => {
        if (p.cat !== sel.cat) return false;
        const pSub   = Array.isArray(p.sub)   ? p.sub   : [];
        const selSub = Array.isArray(sel.sub) ? sel.sub : [];

        // Selección "toda la categoría" (sub vacío) matchea cualquier sub del producto
        if (selSub.length === 0) return true;

        if (pSub.length < selSub.length) return false;
        for (let i = 0; i < selSub.length; i++) {
          if (pSub[i] !== selSub[i]) return false;
        }
        return true;
      });
    });

    if (productosFiltrados.length === 0) {
      return res.status(400).json({ error: 'No hay productos disponibles en las hojas seleccionadas' });
    }

    // Ordenar productos por sección (cat > sub) y luego por referencia
    productosFiltrados.sort((a, b) => {
      if (a.cat !== b.cat) return a.cat.localeCompare(b.cat);
      const aSub = (a.sub || []).join('›');
      const bSub = (b.sub || []).join('›');
      if (aSub !== bSub) return aSub.localeCompare(bSub);
      return String(a.ref).localeCompare(String(b.ref));
    });

    // === Generar el PDF ===
    const filename = `catalogo-infinity-${Date.now()}.pdf`;
    res.setHeader('Content-Type', 'application/pdf');
    res.setHeader('Content-Disposition', `attachment; filename="${filename}"`);

    const doc = new PDFDocument({
      size: 'A4',
      margins: { top: 50, bottom: 50, left: 50, right: 50 },
      info: { Title: 'Catálogo Infinity', Author: 'Infinity', Subject: 'Catálogo de productos' }
    });
    doc.pipe(res);

    const pageW = doc.page.width;
    const pageH = doc.page.height;
    const MARGIN_X = 50;

    // Constantes de layout de productos
    const CARD_W   = 240;
    const CARD_H   = 290;
    const IMG_H    = 180;
    const GUTTER   = 20;
    const TOP_Y    = 110;             // bajo el subtítulo
    const BOTTOM_Y = pageH - 60;      // deja espacio para el footer

    // Helper: nombre legible de la sección (cat > sub1 > sub2)
    function getSeccionNombre(catObj, subArr) {
      const parts = [catObj.nombre];
      let nodo = catObj;
      for (const sid of subArr) {
        const subs = Array.isArray(nodo.subcategorias) ? nodo.subcategorias : [];
        const s = subs.find(x => x.id === sid);
        if (!s) break;
        parts.push(s.nombre);
        nodo = s;
      }
      return parts.join(' › ');
    }

    // Helper: dibuja el header de una sección (subtítulo + línea)
    function drawSeccionHeader(titulo) {
      doc.fontSize(22).fillColor('#8b5cf6').font('Helvetica-Bold')
         .text(titulo, MARGIN_X, 50, { width: pageW - 2 * MARGIN_X });
      doc.moveTo(MARGIN_X, 90).lineTo(pageW - MARGIN_X, 90)
         .strokeColor('rgba(139, 92, 246, 0.5)').lineWidth(1.5).stroke();
    }

    // Helper: dibuja el placeholder si una imagen no se pudo cargar
    function drawImagePlaceholder(x, y) {
      doc.rect(x, y, CARD_W, IMG_H).fillColor('#f5f3ff').fill();
      doc.fontSize(11).fillColor('#8b8ba7').font('Helvetica')
         .text('Sin imagen', x, y + IMG_H / 2 - 6, { width: CARD_W, align: 'center' });
    }

    // Helper: convierte una imagen del catálogo (webp) a un buffer JPG
    async function loadImageBuffer(imgRelPath) {
      const imgPath = path.join(IMG_DIR, imgRelPath);
      const buffer  = await fs.readFile(imgPath);
      return await sharp(buffer).jpeg({ quality: 85 }).toBuffer();
    }

    // Helper: dibuja el footer (número de página)
    function drawPageFooter() {
      const range = doc.bufferedPageRange();
      for (let i = range.start; i < range.start + range.count; i++) {
        doc.switchToPage(i);
        doc.fontSize(8).fillColor('#8b8ba7').font('Helvetica')
           .text(`${i + 1} / ${range.count}`,
                 MARGIN_X, pageH - 40, { width: pageW - 2 * MARGIN_X, align: 'center' });
      }
    }

    // === PORTADA ===
    doc.fontSize(80).fillColor('#8b5cf6').font('Helvetica-Bold')
       .text('∞', 0, 180, { align: 'center', width: pageW });

    doc.fontSize(28).fillColor('#1a1a2e').font('Helvetica-Bold')
       .text('Infinity', 0, 290, { align: 'center', width: pageW });

    doc.moveTo(200, 340).lineTo(pageW - 200, 340)
       .strokeColor('#8b5cf6').lineWidth(2).stroke();

    doc.fontSize(40).fillColor('#1a1a2e').font('Helvetica-Bold')
       .text('Catálogo', 0, 380, { align: 'center', width: pageW });

    const fechaStr = new Date().toLocaleDateString('es-CO', {
      weekday: 'long', year: 'numeric', month: 'long', day: 'numeric'
    });
    doc.fontSize(13).fillColor('#4a4a68').font('Helvetica')
       .text(fechaStr, 0, 450, { align: 'center', width: pageW });

    doc.fontSize(12).fillColor('#8b8ba7').font('Helvetica')
       .text(
         `${productosFiltrados.length} producto${productosFiltrados.length === 1 ? '' : 's'}`,
         0, 510, { align: 'center', width: pageW }
       );

    doc.fontSize(10).fillColor('#8b8ba7').font('Helvetica')
       .text('Generado el ' + new Date().toLocaleString('es-CO'),
             0, pageH - 80, { align: 'center', width: pageW });

    // === SECCIONES DE PRODUCTOS ===
    // Agrupar por sección
    const seccionesMap = new Map();
    for (const p of productosFiltrados) {
      const key = `${p.cat}|${(p.sub || []).join('|')}`;
      if (!seccionesMap.has(key)) {
        seccionesMap.set(key, { cat: p.cat, sub: p.sub || [], productos: [] });
      }
      seccionesMap.get(key).productos.push(p);
    }
    const secciones = Array.from(seccionesMap.values()).sort((a, b) => {
      if (a.cat !== b.cat) return a.cat.localeCompare(b.cat);
      return a.sub.join('|').localeCompare(b.sub.join('|'));
    });

    for (const sec of secciones) {
      const catObj = categorias.find(c => c.id === sec.cat);
      if (!catObj) continue;

      const titulo = getSeccionNombre(catObj, sec.sub);

      // Nueva página para la sección
      doc.addPage();
      drawSeccionHeader(titulo);

      // Pre-cargar imágenes en paralelo
      const productosConBuffer = await Promise.all(
        sec.productos.map(async p => {
          try {
            return { prod: p, imgBuffer: await loadImageBuffer(p.img) };
          } catch (err) {
            console.error('Error cargando imagen para PDF:', p.img, err.message);
            return { prod: p, imgBuffer: null };
          }
        })
      );

      let col = 0;
      let y   = TOP_Y;

      for (const { prod, imgBuffer } of productosConBuffer) {
        const x = MARGIN_X + col * (CARD_W + GUTTER);

        // Salto de página si no cabe (y repetimos subtítulo)
        if (y + CARD_H > BOTTOM_Y) {
          doc.addPage();
          drawSeccionHeader(titulo);
          y   = TOP_Y;
          col = 0;
        }

        // Foto
        if (imgBuffer) {
          try {
            doc.image(imgBuffer, x, y, { fit: [CARD_W, IMG_H], align: 'center', valign: 'center' });
          } catch {
            drawImagePlaceholder(x, y);
          }
        } else {
          drawImagePlaceholder(x, y);
        }

        // Referencia
        let textY = y + IMG_H + 8;
        doc.fontSize(8).fillColor('#8b8ba7').font('Helvetica')
           .text(`REF: ${prod.ref || '—'}`, x, textY, { width: CARD_W });

        // Nombre
        textY += 12;
        doc.fontSize(11).fillColor('#1a1a2e').font('Helvetica-Bold')
           .text(prod.nombre || prod.ref || 'Sin nombre', x, textY, { width: CARD_W, height: 50 });

        // Precio
        textY += 52;
        const precio = (prod.precio !== undefined && prod.precio !== null && prod.precio !== '')
          ? `$${Number(prod.precio).toLocaleString('es-CO')}`
          : '—';
        doc.fontSize(15).fillColor('#8b5cf6').font('Helvetica-Bold')
           .text(precio, x, textY, { width: CARD_W });

        col++;
        if (col >= 2) {
          col = 0;
          y += CARD_H + 20;
        }
      }
    }

    // === FOOTER con número de página ===
    drawPageFooter();

    doc.end();
  } catch (err) {
    console.error('PDF generation error:', err);
    if (!res.headersSent) {
      res.status(500).json({ error: 'Error al generar el PDF: ' + err.message });
    }
  }
});

// ════════════════════════════════
// DESCARGAS — FOTOS (ZIP)
// ════════════════════════════════
app.post('/api/descargas/fotos', requireDescargasAccess, async (req, res) => {
  const { seleccion } = req.body || {};

  if (!Array.isArray(seleccion) || seleccion.length === 0) {
    return res.status(400).json({ error: 'Debes seleccionar al menos una categoría o subcategoría' });
  }

  try {
    const categorias = await getCategorias();
    const productos  = await getProductos();

    // Validar selección (misma lógica que PDF)
    for (const item of seleccion) {
      if (!item || typeof item.cat !== 'string') {
        return res.status(400).json({ error: 'Selección inválida' });
      }
      const cat = categorias.find(c => c.id === item.cat);
      if (!cat) {
        return res.status(400).json({ error: `Categoría "${item.cat}" no existe` });
      }
      if (Array.isArray(item.sub) && item.sub.length > 0) {
        let nodo = cat;
        for (let i = 0; i < item.sub.length; i++) {
          const subs = Array.isArray(nodo.subcategorias) ? nodo.subcategorias : [];
          const sig = subs.find(s => s.id === item.sub[i]);
          if (!sig) {
            return res.status(400).json({ error: `Subcategoría "${item.sub[i]}" no encontrada` });
          }
          nodo = sig;
        }
      }
    }

    // Filtrar productos (misma lógica que PDF)
    const productosFiltrados = productos.filter(p => {
      if (!p.cat || p.cat === SIN_CATEGORIA_ID) return false;
      if (!p.img) return false;
      if (p.img.startsWith('agotados/')) return false;

      return seleccion.some(sel => {
        if (p.cat !== sel.cat) return false;
        const pSub   = Array.isArray(p.sub)   ? p.sub   : [];
        const selSub = Array.isArray(sel.sub) ? sel.sub : [];
        if (selSub.length === 0) return true;
        if (pSub.length < selSub.length) return false;
        for (let i = 0; i < selSub.length; i++) {
          if (pSub[i] !== selSub[i]) return false;
        }
        return true;
      });
    });

    if (productosFiltrados.length === 0) {
      return res.status(400).json({ error: 'No hay productos disponibles en las hojas seleccionadas' });
    }

    // Stream ZIP
    const filename = `fotos-infinity-${Date.now()}.zip`;
    res.setHeader('Content-Type', 'application/zip');
    res.setHeader('Content-Disposition', `attachment; filename="${filename}"`);

    const archive = archiver('zip', { zlib: { level: 5 } });

    archive.on('error', err => {
      console.error('ZIP error:', err.message);
      if (!res.headersSent) {
        res.status(500).json({ error: 'Error al generar el ZIP: ' + err.message });
      } else {
        res.end();
      }
    });

    archive.pipe(res);

    for (const p of productosFiltrados) {
      const imgRelPath = p.img;
      const imgAbsPath = path.join(IMG_DIR, imgRelPath);
      const ext        = path.extname(imgRelPath) || '.webp';
      const arcName    = `${p.ref || 'producto'}${ext}`;

      try {
        await fs.access(imgAbsPath);
        archive.file(imgAbsPath, { name: arcName });
      } catch {
        console.warn(`[fotos] Imagen no encontrada: ${imgAbsPath}`);
      }
    }

    await archive.finalize();
  } catch (err) {
    console.error('Fotos ZIP generation error:', err);
    if (!res.headersSent) {
      res.status(500).json({ error: 'Error al generar el ZIP: ' + err.message });
    }
  }
});

// ════════════════════════════════
// PEDIDOS — generar PDF con pedido
// ════════════════════════════════
app.post('/api/pedidos/generar-pdf', async (req, res) => {
  const { items, cliente } = req.body || {};

  if (!Array.isArray(items) || items.length === 0) {
    return res.status(400).json({ error: 'El pedido debe tener al menos un producto' });
  }
  if (!cliente || !cliente.nombre || !cliente.ciudad || !cliente.telefono || !cliente.direccion) {
    return res.status(400).json({ error: 'Todos los datos del cliente son obligatorios' });
  }

  try {
    const filename = `pedido-${Date.now()}.pdf`;
    const filepath = path.join(PEDIDOS_DIR, filename);

    // Asegurar que la carpeta existe
    await fs.mkdir(PEDIDOS_DIR, { recursive: true });

    const doc = new PDFDocument({
      size: 'A4',
      margins: { top: 50, bottom: 50, left: 50, right: 50 },
      info: {
        Title: `Pedido - ${cliente.nombre}`,
        Author: 'Infinity',
        Subject: 'Pedido de productos'
      }
    });

    const writeStream = fsSync.createWriteStream(filepath);
    doc.pipe(writeStream);

    const pageW = doc.page.width;
    const MARGIN_X = 50;
    const pageH = doc.page.height;

    // === ENCABEZADO ===
    doc.fontSize(36).fillColor('#8b5cf6').font('Helvetica-Bold')
       .text('∞ Infinity', MARGIN_X, 50, { width: pageW - 2 * MARGIN_X });

    doc.moveTo(MARGIN_X, 100).lineTo(pageW - MARGIN_X, 100)
       .strokeColor('#8b5cf6').lineWidth(1.5).stroke();

    doc.fontSize(22).fillColor('#1a1a2e').font('Helvetica-Bold')
       .text('Pedido', MARGIN_X, 120, { width: pageW - 2 * MARGIN_X });

    const fechaStr = new Date().toLocaleDateString('es-CO', {
      weekday: 'long', year: 'numeric', month: 'long', day: 'numeric',
      hour: '2-digit', minute: '2-digit'
    });
    doc.fontSize(10).fillColor('#8b8ba7').font('Helvetica')
       .text(fechaStr, MARGIN_X, 150, { width: pageW - 2 * MARGIN_X });

    // === DATOS DEL CLIENTE ===
    doc.fontSize(14).fillColor('#1a1a2e').font('Helvetica-Bold')
       .text('Datos del cliente', MARGIN_X, 185, { width: pageW - 2 * MARGIN_X });

    const clientFields = [
      ['Nombre',  cliente.nombre],
      ['Ciudad',  cliente.ciudad],
      ['Teléfono', cliente.telefono],
      ['Dirección', cliente.direccion]
    ];

    let clientY = 210;
    doc.fontSize(11).fillColor('#4a4a68').font('Helvetica');
    for (const [label, value] of clientFields) {
      doc.text(`${label}:  `, MARGIN_X, clientY, { continued: true });
      doc.fillColor('#1a1a2e').text(value, { width: pageW - 2 * MARGIN_X });
      clientY += 18;
    }

    // === TABLA DE PRODUCTOS ===
    let tableY = clientY + 20;
    doc.moveTo(MARGIN_X, tableY).lineTo(pageW - MARGIN_X, tableY)
       .strokeColor('#e8e6f0').lineWidth(0.5).stroke();

    tableY += 12;
    const colRef     = MARGIN_X;
    const colNombre  = MARGIN_X + 80;
    const colCant    = pageW - MARGIN_X - 180;
    const colPrecio  = pageW - MARGIN_X - 120;
    const colSubtotal = pageW - MARGIN_X - 60;

    doc.fontSize(9).fillColor('#8b8ba7').font('Helvetica-Bold');
    doc.text('REF',    colRef, tableY);
    doc.text('Producto', colNombre, tableY);
    doc.text('Cant',   colCant, tableY, { align: 'center', width: 40 });
    doc.text('Precio', colPrecio, tableY, { align: 'right', width: 60 });
    doc.text('Total',  colSubtotal, tableY, { align: 'right', width: 60 });

    const headerY = tableY;

    let totalGeneral = 0;
    for (const item of items) {
      const subtotal = (item.precio || 0) * (item.cantidad || 1);
      totalGeneral += subtotal;

      tableY += 22;
      if (tableY > pageH - 90) {
        doc.addPage();
        tableY = 60;
      }

      doc.fontSize(9).fillColor('#8b8ba7').font('Helvetica')
         .text(String(item.ref || '—'), colRef, tableY, { width: 76 });
      doc.fillColor('#1a1a2e').font('Helvetica')
         .text(String(item.nombre || item.ref || '—'), colNombre, tableY, { width: colCant - colNombre - 10 });
      doc.fillColor('#1a1a2e').font('Helvetica-Bold')
         .text(String(item.cantidad || 1), colCant, tableY, { align: 'center', width: 40 });
      doc.fillColor('#4a4a68').font('Helvetica')
         .text(`$${(item.precio || 0).toLocaleString('es-CO')}`, colPrecio, tableY, { align: 'right', width: 60 });
      doc.fillColor('#8b5cf6').font('Helvetica-Bold')
         .text(`$${subtotal.toLocaleString('es-CO')}`, colSubtotal, tableY, { align: 'right', width: 60 });
    }

    // Línea final
    tableY += 20;
    doc.moveTo(MARGIN_X, tableY).lineTo(pageW - MARGIN_X, tableY)
       .strokeColor('#e8e6f0').lineWidth(0.5).stroke();

    tableY += 14;
    doc.fontSize(11).fillColor('#4a4a68').font('Helvetica');
    doc.text('Total pedido:', MARGIN_X, tableY, { continued: true });
    doc.fontSize(16).fillColor('#8b5cf6').font('Helvetica-Bold')
       .text(`  $${totalGeneral.toLocaleString('es-CO')}`, { width: pageW - 2 * MARGIN_X });

    // === FOOTER con número de página ===
    const range = doc.bufferedPageRange();
    for (let i = range.start; i < range.start + range.count; i++) {
      doc.switchToPage(i);
      doc.fontSize(8).fillColor('#8b8ba7').font('Helvetica')
         .text(`${i + 1} / ${range.count}`,
               MARGIN_X, pageH - 40, { width: pageW - 2 * MARGIN_X, align: 'center' });
    }

    doc.end();

    // Esperar a que termine de escribir
    await new Promise((resolve, reject) => {
      writeStream.on('finish', resolve);
      writeStream.on('error', reject);
    });

    // Enviar el archivo
    res.setHeader('Content-Type', 'application/pdf');
    res.setHeader('Content-Disposition', `attachment; filename="${filename}"`);
    res.sendFile(filepath);
  } catch (err) {
    console.error('Pedido PDF error:', err);
    res.status(500).json({ error: 'Error al generar el pedido: ' + err.message });
  }
});

// ════════════════════════════════
// START
// ════════════════════════════════
app.listen(PORT, () => {
  console.log(`∞ Infinity server corriendo en http://localhost:${PORT}`);
  scheduleSessionCleanup();
});
