// El ASISTENTE del cerebrito: un mini servidor local (solo 127.0.0.1) con la
// pagina de configuracion y estado. Primera vez: entrar con la cuenta del
// negocio, buscar impresoras, probar impresion y guardar. Despues: pantalla de
// estado con reimprimir ultima, prueba, arranque con Windows y el mando
// titular/suplente. Sin dependencias: node:http + HTML propio.
import { execFile } from 'node:child_process';
import { writeFileSync } from 'node:fs';
import { createServer } from 'node:http';
import { join } from 'node:path';
import { accesoDirectoApp } from './ventanaApp.js';
import { comandaABytes } from './escpos.js';
import { buscarImpresoras, ipLocal } from './escaner.js';
import { enviarTcp } from './impresoras.js';
import { enviarUsb, listarImpresorasUsb, instalarImpresoraAuto } from './usb.js';
import { cargarConfigNube, guardarConfigNube } from './nubeConfig.js';
import type { Config, ImpresoraCfg, LineaComanda } from './tipos.js';

export const PUERTO_ASISTENTE = 8737;
const DESPACHO = '__despacho';

interface MotorRef {
  nombreRestaurante: string;
  estado(): { canalVivo: boolean; ultimas: string[]; cola: { pendientes: number; fallidas: number } };
  reimprimirUltima(): string | null;
  reintentarFallidas(): number;
  puestos(): { id: string; nombre: string }[];
}

export interface Contexto {
  base: string;
  esExe: boolean;
  urlSupabase(): string;
  anonKey(): string;
  obtenerConfig(): Config | null;
  guardarConfig(cfg: Config): void;
  motor(): MotorRef | null;
  reiniciarMotor(): Promise<string | null>;
  logs(): string[];
  log(m: string): void;
  comandoArranque(): string;
  /** Estado de la etapa 2 (salon local, POS, sincronizador y actualizador) para la pagina. */
  extras(): {
    puertoPos: number;
    pinListo: boolean;
    salon: { mesasAbiertas: number; ventasPorSubir: number } | null;
    nube: { nubeViva: boolean; ultimoLatido: string | null; ventasPorSubir: number } | null;
    espejoListo: boolean;
    version: { actual: string; disponible: string | null; descargando: boolean };
    fiscal: {
      configurada: boolean;
      conectada: boolean | null;
      negocio: string | null;
      rif: string | null;
      serial: string | null;
      porSellar: number;
      ultimoSello: string | null;
      problema: string | null;
    };
  };
  aplicarActualizacion(): Promise<string | null>;
  /** Revisa si hay version nueva (el asistente lo pide al abrir la pagina, con freno). */
  verificarActualizacion?(): void;
  /** El apreton de manos con la Estacion Fiscal (probar la llave al conectarla). */
  probarEstacionFiscal(): Promise<{ ok: boolean; negocio?: string | null; rif?: string | null; problema?: string }>;
  /** Entregar de una lo que haya por sellar (tras conectar, sin esperar la ronda). */
  dispararPuenteFiscal(): void;
}

interface Borrador {
  correo: string;
  clave: string;
  token: string;
  restauranteId: string;
  restaurante: string;
  puestos: { id: string; nombre: string }[];
  /** Config de impresion guardada en la nube (baja con la cuenta). */
  configNube: import('./nubeConfig.js').PrefsNube | null;
}

function ejecutar(cmd: string, args: string[]): Promise<{ ok: boolean; salida: string }> {
  return new Promise((resolve) => {
    execFile(cmd, args, { windowsHide: true }, (err, stdout, stderr) =>
      resolve({ ok: !err, salida: String(stdout) + String(stderr) }),
    );
  });
}

const RUN_KEY = 'HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run';
const RUN_VAL = 'CerebritoGustito';

async function autoarranqueActivo(): Promise<boolean> {
  const r = await ejecutar('reg', ['query', RUN_KEY, '/v', RUN_VAL]);
  return r.ok;
}

async function ponerAutoarranque(ctx: Contexto, activar: boolean): Promise<void> {
  if (!activar) {
    await ejecutar('reg', ['delete', RUN_KEY, '/v', RUN_VAL, '/f']);
    return;
  }
  // Un .vbs lo arranca OCULTO (sin ventana negra); el estado se ve en esta pagina.
  // OJO: en VBScript las comillas dentro de un string se escapan DUPLICANDOLAS ("");
  // JSON.stringify escapa con \" y \\ y produce un .vbs que no compila (error 800A0401
  // de Windows Script Host en cada inicio de sesion, y el cerebrito nunca arranca).
  const vbs = join(ctx.base, 'arrancar_oculto.vbs');
  const comandoVbs = `"${ctx.comandoArranque().replace(/"/g, '""')}"`;
  writeFileSync(vbs, `CreateObject("Wscript.Shell").Run ${comandoVbs}, 0, False\r\n`);
  await ejecutar('reg', ['add', RUN_KEY, '/v', RUN_VAL, '/t', 'REG_SZ', '/d', `wscript.exe "${vbs}"`, '/f']);
}


function ticketPrueba(nombre: string): LineaComanda[] {
  const hora = new Date();
  const hh = `${String(hora.getDate()).padStart(2, '0')}/${String(hora.getMonth() + 1).padStart(2, '0')}/${hora.getFullYear()}`;
  return [
    { texto: 'PRUEBA DE IMPRESION', grande: true, negrita: true, centrada: true },
    { texto: nombre, grande: true, centrada: true },
    { texto: hh, centrada: true },
    { texto: '-'.repeat(48) },
    { texto: 'Si lees esto, la impresora quedo conectada', centrada: true },
    { texto: 'al cerebrito de Gustito Xpress.', centrada: true },
    { texto: '-'.repeat(48) },
    { texto: 'Gustito Xpress', centrada: true },
  ];
}

async function loginNegocio(ctx: Contexto, correo: string, clave: string): Promise<Borrador | { error: string }> {
  const URL = ctx.urlSupabase();
  const ANON = ctx.anonKey();
  if (!URL || !ANON) return { error: 'Este programa no trae la configuracion del servidor (build sin llaves).' };
  const r = await fetch(`${URL}/auth/v1/token?grant_type=password`, {
    method: 'POST',
    headers: { apikey: ANON, 'Content-Type': 'application/json' },
    body: JSON.stringify({ email: correo, password: clave }),
  });
  if (!r.ok) return { error: 'Correo o clave incorrectos.' };
  const ses = (await r.json()) as { access_token?: string; user?: { id?: string } };
  const token = ses.access_token ?? '';
  const uid = ses.user?.id ?? '';
  const auth = { apikey: ANON, Authorization: `Bearer ${token}` };
  const pR = await fetch(`${URL}/rest/v1/perfiles?id=eq.${uid}&select=rol,restaurante_id`, { headers: auth });
  const perfil = ((pR.ok ? await pR.json() : []) as { rol?: string; restaurante_id?: string }[])[0];
  if (!perfil?.restaurante_id || !['dueno', 'encargado', 'admin'].includes(perfil.rol ?? '')) {
    return { error: 'Usa la cuenta del dueño o de un encargado del negocio (no la de un mesero).' };
  }
  const rR = await fetch(`${URL}/rest/v1/restaurantes?id=eq.${perfil.restaurante_id}&select=nombre`, { headers: auth });
  const rest = ((rR.ok ? await rR.json() : []) as { nombre?: string }[])[0];
  const puR = await fetch(
    `${URL}/rest/v1/puestos_impresion?restaurante_id=eq.${perfil.restaurante_id}&activo=eq.true&select=id,nombre&order=orden`,
    { headers: auth },
  );
  const puestos = (puR.ok ? await puR.json() : []) as { id: string; nombre: string }[];
  const configNube = await cargarConfigNube(URL, ANON, token, perfil.restaurante_id);
  return {
    correo,
    clave,
    token,
    restauranteId: perfil.restaurante_id,
    restaurante: rest?.nombre ?? 'tu negocio',
    puestos,
    configNube,
  };
}

// ============================== la pagina ==================================
// El molde del tablero (el mismo de la Estacion Fiscal y el panel de la
// imprenta): azul noche + dorado, a lo ancho, barra superior con la marca.
const PAGINA = `<!doctype html><html lang="es"><head><meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="theme-color" content="#0B0D14">
<link rel="icon" type="image/svg+xml" href="/icono.svg">
<link rel="manifest" href="/manifest.webmanifest">
<title>Cerebrito Gustito</title>
<style>
  * { box-sizing: border-box; margin: 0; }
  :root { --fondo:#0B0D14; --panel:#131624; --panel2:#1A1E30; --borde:#2A2F4A; --texto:#FFFFFF; --texto2:#D6DAEA; --oro:#FBBF24; --verde:#34D399; --rojo:#F87171; }
  body { background:var(--fondo); color:var(--texto); font-family:'Segoe UI',system-ui,sans-serif; font-size:15px; line-height:1.5; }
  .sub { color:var(--texto2); font-size:13px; }
  h2 { font-size:12px; font-weight:800; text-transform:uppercase; letter-spacing:.14em; color:var(--texto2); margin:22px 0 8px; }
  p.ayuda { color:var(--texto2); font-size:13.5px; margin-bottom:12px; }
  .tarjeta { background:var(--panel); border:1px solid var(--borde); border-radius:12px; padding:16px 18px; margin-top:14px; }
  input, select { width:100%; background:var(--fondo); color:var(--texto); border:1px solid var(--borde); border-radius:8px; padding:10px 12px; font-size:14px; }
  input:focus, select:focus { outline:none; border-color:var(--oro); }
  label { display:block; font-size:12px; color:var(--texto2); font-weight:700; margin:10px 0 4px; }
  button { background:var(--oro); color:#0B0D14; border:none; border-radius:9px; padding:11px 16px; font-size:13.5px; font-weight:800; cursor:pointer; }
  button.sec { background:transparent; color:var(--texto2); border:1px solid var(--borde); font-weight:600; }
  button.sec:hover { color:var(--texto); }
  button:disabled { opacity:.75; cursor:progress; }
  .fila { display:flex; gap:8px; align-items:center; flex-wrap:wrap; }
  .error { color:var(--rojo); font-size:13.5px; margin-top:10px; }
  .ok { color:var(--verde); font-size:13.5px; margin-top:10px; }
  .imp { border:1px solid var(--borde); border-radius:12px; padding:12px; margin-top:10px; display:flex; flex-direction:column; align-items:stretch; gap:4px; }
  .checks { display:flex; flex-wrap:wrap; gap:7px 16px; margin-top:2px; }
  .chk { display:inline-flex; align-items:center; gap:6px; font-size:13.5px; color:var(--texto); cursor:pointer; }
  .chk input { width:16px; height:16px; accent-color:var(--oro); cursor:pointer; }
  .imp .dir { font-family:Consolas,monospace; font-size:13.5px; }
  .imp select { width:auto; min-width:170px; }
  .pill { display:inline-block; border-radius:6px; padding:4px 11px; font-size:11.5px; font-weight:800; letter-spacing:.07em; text-transform:uppercase; }
  .pill.v { background:rgba(52,211,153,.12); color:var(--verde); border:1px solid rgba(52,211,153,.35); }
  .pill.r { background:rgba(248,113,113,.1); color:var(--rojo); border:1px solid rgba(248,113,113,.35); }
  .pill.a { background:rgba(251,191,36,.1); color:var(--oro); border:1px solid rgba(251,191,36,.35); }
  .log { background:#0a0c15; border:1px solid var(--borde); border-radius:10px; padding:10px 12px; font-family:Consolas,monospace; font-size:11.5px; color:var(--texto2); max-height:220px; overflow:auto; white-space:pre-wrap; margin-top:10px; }
  .grande { font-size:16px; font-weight:800; }
  .aviso-reserva { background:rgba(251,191,36,.07); border:1px solid rgba(251,191,36,.4); border-radius:12px; padding:14px; margin-top:14px; }
  a { color:var(--oro); }
  /* Barra superior */
  .topbar { background:var(--panel); border-bottom:1px solid var(--borde); padding:10px 22px; display:flex; align-items:center; gap:16px; flex-wrap:wrap; }
  .topbar .cubo { width:38px; height:38px; border-radius:9px; background:linear-gradient(135deg,var(--oro),#B45309); display:grid; place-items:center; font-weight:900; color:#0B0D14; font-size:13px; flex:none; }
  .topbar .nombre b { font-size:16px; }
  .topbar .nombre span { display:block; font-size:11.5px; color:var(--texto2); font-family:Consolas,monospace; }
  .topbar .quien { margin-left:auto; display:flex; align-items:center; gap:8px; font-size:12.5px; color:var(--texto2); flex-wrap:wrap; }
  /* Contenido: tablero a lo ancho, 2 o 3 columnas segun la pantalla */
  #app.contenido { padding:18px 22px 60px; }
  #app.paso { max-width:880px; margin:0 auto; padding:18px 22px 60px; }
  @media (min-width:920px) {
    /* stretch: las tarjetas de cada fila quedan PAREJAS, sin huecos raros */
    #app.contenido.amplia { display:grid; grid-template-columns:1fr 1fr; gap:14px; align-items:stretch; }
    #app.contenido.amplia > * { margin-top:0; }
    #app.contenido.amplia > .ancho { grid-column:1/-1; }
  }
  @media (min-width:1500px) { #app.contenido.amplia { grid-template-columns:1fr 1fr 1fr; } }
  /* La antesala (paso 1): marca a la izquierda, la cuenta a la derecha */
  #app.puerta { padding:0; }
  .antesala { display:grid; grid-template-columns:1.1fr 1fr; min-height:100vh; }
  .lado-marca { background:radial-gradient(700px 460px at 35% 42%,#1A1E30 0%,#0B0D14 62%); border-right:1px solid var(--borde); display:flex; flex-direction:column; align-items:center; justify-content:center; gap:20px; padding:40px; }
  .lado-marca .emblema { width:118px; height:118px; border-radius:26px; background:linear-gradient(135deg,var(--oro),#B45309); display:grid; place-items:center; font-weight:900; color:#0B0D14; font-size:38px; box-shadow:0 0 90px rgba(251,191,36,.22); }
  .lado-marca h1 { font-size:30px; text-align:center; }
  .lado-marca .subm { color:var(--texto2); font-size:13.5px; font-family:Consolas,monospace; text-align:center; }
  .lado-marca .raya { width:46px; height:3px; border-radius:2px; background:var(--oro); opacity:.85; }
  .lado-form { display:flex; align-items:center; justify-content:center; padding:40px 24px; }
  .form-tarjeta { width:100%; max-width:430px; background:var(--panel); border:1px solid var(--borde); border-radius:16px; padding:34px 36px 30px; }
  .form-tarjeta h1 { font-size:22px; }
  .form-tarjeta .subf { color:var(--texto2); font-size:13.5px; margin:6px 0 10px; line-height:1.6; }
  .form-tarjeta input { padding:13px 12px; font-size:15px; border-radius:10px; }
  .form-tarjeta .entrar { width:100%; padding:12px; font-size:14.5px; margin-top:20px; }
  @media (max-width:900px) { .antesala { grid-template-columns:1fr; } .lado-marca { display:none; } }
</style></head><body>
<div id="barra"></div>
<div id="app" class="contenido"><p class="ayuda" style="margin-top:20px">Cargando...</p></div>
<script>
const $ = (h) => { const d = document.createElement('div'); d.innerHTML = h; return d; };
const app = document.getElementById('app');
let E = null; // estado del servidor
let borrador = null; // {restaurante, puestos:[]}
let impresoras = []; // {host, puerto, asignacion}
let guardarForzado = false; // el usuario aceptó guardar sin impresora de cocina

// api() NUNCA lanza: si lanzara, el boton que quedo deshabilitado antes del
// await jamas se vuelve a habilitar (se queda gris para siempre).
async function api(ruta, cuerpo) {
  try {
    const r = await fetch('/api/' + ruta, cuerpo ? { method: 'POST', headers: {'Content-Type':'application/json'}, body: JSON.stringify(cuerpo) } : undefined);
    return await r.json();
  } catch (e) {
    return { error: 'El cerebrito no respondio. Intenta de nuevo.' };
  }
}

function esc(s) { return String(s ?? '').replace(/[&<>"]/g, (c) => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c])); }

// La barra superior: la marca siempre, el subtitulo segun la pantalla y a la
// derecha lo que toque (pills de estado, version).
function barraSuperior(sub, derecha) {
  document.getElementById('barra').innerHTML =
    '<div class="topbar"><div class="cubo">CG</div>' +
    '<div class="nombre"><b>Cerebrito Gustito</b><span>' + esc(sub || 'La caja de impresion de comandas') + '</span></div>' +
    '<div class="quien">' + (derecha || '') + '</div></div>';
}

// Agrupa entradas de config (una por puesto) en UNA por impresora fisica, con la
// lista de puestos que saca. Si el despacho apunta a un puesto de esa impresora,
// marca tambien "__despacho" (Respaldo / Despacho).
function agruparImpresoras(lista, despacho) {
  const byHost = {};
  const despPuesto = despacho && despacho.puesto;
  for (const it of (lista || [])) {
    const tipo = it.tipo === 'usb' ? 'usb' : 'tcp';
    const key = tipo + '|' + it.host;
    if (!byHost[key]) byHost[key] = { tipo: tipo, host: it.host, puerto: it.puerto || (tipo === 'usb' ? 0 : 9100), puestos: [] };
    const nombre = it.puesto === 'Despacho' ? '__despacho' : it.puesto;
    if (nombre && !byHost[key].puestos.includes(nombre)) byHost[key].puestos.push(nombre);
    if (despPuesto && it.puesto === despPuesto && !byHost[key].puestos.includes('__despacho')) byHost[key].puestos.push('__despacho');
  }
  return Object.keys(byHost).map((k) => byHost[k]);
}

// UNA fila por impresora fisica. Cada impresora lista con CASILLAS lo que saca:
// los puestos de cocina (ej. Carrito de arepas, Cocina) y "Respaldo / Despacho".
// Una misma maquina puede tener varias marcadas (asi una sola impresora saca
// papeles separados por puesto). impresoras[i].puestos = string[] (incluye
// '__despacho' para el ticket de respaldo/despacho).
function pintarImpresoras(idLista) {
  const cont = document.getElementById(idLista);
  cont.innerHTML = '';
  const puestos = (borrador?.puestos ?? E?.puestos ?? []);
  if (!impresoras.length) { cont.innerHTML = '<p class="sub" style="padding:6px 2px">Aún no hay impresoras. Toca "Instalar mi impresora" o "Buscar USB".</p>'; return; }
  impresoras.forEach((imp, i) => {
    if (!Array.isArray(imp.puestos)) imp.puestos = [];
    const dir = imp.tipo === 'usb' ? 'USB · ' + esc(imp.host) : esc(imp.host) + ':' + imp.puerto;
    let checks = puestos.map((p) =>
      '<label class="chk"><input type="checkbox" data-i="' + i + '" data-p="' + esc(p.nombre) + '"' + (imp.puestos.includes(p.nombre) ? ' checked' : '') + '> ' + esc(p.nombre) + '</label>',
    ).join('');
    checks += '<label class="chk"><input type="checkbox" data-i="' + i + '" data-p="__despacho"' + (imp.puestos.includes('__despacho') ? ' checked' : '') + '> Despacho / Copia-Negocio</label>';
    const fila = $('<div class="imp">' +
      '<div class="fila" style="justify-content:space-between;width:100%">' +
        '<span class="dir grande">' + dir + '</span>' +
        '<span class="fila"><button class="sec" data-probar="' + i + '">Probar</button>' +
        '<button class="sec" data-quitar="' + i + '">Quitar</button><span id="res-' + i + '"></span></span>' +
      '</div>' +
      '<div class="sub" style="margin:8px 0 4px">Marca lo que imprime esta máquina:</div>' +
      '<div class="checks">' + checks + '</div>' +
    '</div>').firstChild;
    cont.appendChild(fila);
  });
  cont.querySelectorAll('.checks input').forEach((c) => c.onchange = () => {
    const i = Number(c.dataset.i); const p = c.dataset.p; const arr = impresoras[i].puestos;
    if (c.checked) { if (!arr.includes(p)) arr.push(p); } else { const k = arr.indexOf(p); if (k >= 0) arr.splice(k, 1); }
  });
  cont.querySelectorAll('[data-probar]').forEach((b) => b.onclick = async () => {
    const i = Number(b.dataset.probar); const r = document.getElementById('res-' + i);
    r.innerHTML = '<span class="pill a">imprimiendo...</span>';
    const j = await api('probar', { host: impresoras[i].host, puerto: impresoras[i].puerto, tipo: impresoras[i].tipo || 'tcp' });
    r.innerHTML = j.ok ? '<span class="pill v">salio el papel</span>' : '<span class="pill r">' + esc(j.error || 'no respondio') + '</span>';
  });
  cont.querySelectorAll('[data-quitar]').forEach((b) => b.onclick = () => { impresoras.splice(Number(b.dataset.quitar), 1); pintarImpresoras(idLista); });
}

function vistaAsistente() {
  document.getElementById('barra').innerHTML = '';
  app.className = 'puerta';
  app.innerHTML =
    '<div class="antesala">' +
      '<div class="lado-marca"><div class="emblema">CG</div>' +
      '<div><h1>Cerebrito Gustito</h1><div class="subm">La caja del negocio, con o sin internet</div></div>' +
      '<div class="raya"></div></div>' +
      '<div class="lado-form"><div class="form-tarjeta">' +
        '<h1>Bienvenido</h1>' +
        '<p class="subf">Entra con la cuenta del dueño o del encargado: el cerebrito queda amarrado a tu negocio.</p>' +
        '<label>Correo</label><input id="correo" type="email" autocomplete="username">' +
        '<label>Clave</label><input id="clave" type="password" autocomplete="current-password">' +
        '<div id="err1" class="error"></div>' +
        '<button id="entrar" class="entrar">Continuar</button>' +
      '</div></div>' +
    '</div>';
  document.getElementById('clave').onkeydown = (e) => { if (e.key === 'Enter') document.getElementById('entrar').click(); };
  document.getElementById('entrar').onclick = async () => {
    const b = document.getElementById('entrar'); b.disabled = true; b.textContent = 'Verificando...';
    const j = await api('entrar', { correo: document.getElementById('correo').value.trim(), clave: document.getElementById('clave').value });
    b.disabled = false; b.textContent = 'Continuar';
    if (j.error) { document.getElementById('err1').textContent = j.error; return; }
    borrador = j; impresoras = []; vistaImpresoras();
  };
}

function vistaImpresoras() {
  app.className = 'paso';
  barraSuperior('Paso 2 · Impresoras de ' + (borrador && borrador.restaurante ? borrador.restaurante : 'tu negocio'), '');
  guardarForzado = false;
  const sinPuestos = (borrador.puestos ?? []).length === 0;
  // Baja de la nube: si esta laptop aun no tiene impresoras cargadas pero el
  // negocio ya guardo su configuracion antes (en otra laptop), la precargamos con
  // el mismo mapeo impresora->puesto. El usuario solo confirma que la impresora
  // esta conectada (o toca "Instalar mi impresora") y guarda.
  let bajadaDeNube = false;
  if (impresoras.length === 0 && (borrador.configNube?.impresoras ?? []).length) {
    impresoras = agruparImpresoras(borrador.configNube.impresoras, borrador.configNube.despacho);
    bajadaDeNube = true;
  }
  app.innerHTML =
    '<h2>Paso 2 · Impresoras de ' + esc(borrador.restaurante) + '</h2>' +
    '<p class="ayuda">Prende las impresoras, conectalas a la red del local y busca. A cada una dile qué imprime.</p>' +
    (bajadaDeNube ? '<div class="aviso-reserva">✓ Bajamos la configuración guardada de <b>' + esc(borrador.restaurante) + '</b>. Si la impresora está conectada, toca <b>Instalar mi impresora</b> y luego <b>Guardar y arrancar</b>. No tienes que armar todo de nuevo.</div>' : '') +
    (sinPuestos ? '<div class="aviso-reserva">Tu negocio aún no tiene puestos de cocina creados. Créalos en el panel (Configuración → Comandas por puesto) y toca "Actualizar puestos". Mientras, puedes configurar la de Despacho.</div>' : '') +
    '<p class="ayuda" style="margin:2px 0 10px">Si conectaste la impresora por USB y no la ves, toca <b>Instalar mi impresora</b>: el sistema la detecta e instala sola, aunque Windows no haya hecho nada.</p>' +
    '<div class="tarjeta"><div class="fila"><button id="instalarUsb">Instalar mi impresora (USB)</button>' +
    '<button class="sec" id="buscar">Buscar en la red</button>' +
    '<button class="sec" id="buscarUsb">Buscar USB</button>' +
    '<button class="sec" id="refrescarPuestos">Actualizar puestos</button><span id="prog" class="sub"></span></div>' +
    '<div id="lista"></div>' +
    '<div class="fila" style="margin-top:14px"><input id="manualHost" placeholder="O escribe la dirección (ej: 192.168.1.200)" style="flex:1;min-width:220px">' +
    '<input id="manualPuerto" value="9100" style="width:90px"><button class="sec" id="agregar">Agregar</button></div>' +
    '<div style="margin-top:16px"><button id="guardar">Guardar y arrancar</button></div><div id="err2" class="error"></div></div>';
  pintarImpresoras('lista');
  document.getElementById('buscar').onclick = async () => {
    const p = document.getElementById('prog'); p.textContent = 'Buscando en la red (medio minuto)...';
    const j = await api('escanear', {});
    p.textContent = 'Encontradas: ' + j.ips.length;
    for (const ip of j.ips) if (!impresoras.some((x) => x.host === ip)) impresoras.push({ tipo: 'tcp', host: ip, puerto: 9100, puestos: [] });
    pintarImpresoras('lista');
  };
  document.getElementById('buscarUsb').onclick = async () => {
    const p = document.getElementById('prog'); p.textContent = 'Buscando impresoras USB...';
    const j = await api('usb', {});
    p.textContent = j.nombres.length ? 'USB encontradas: ' + j.nombres.length : 'Sin impresoras USB instaladas en Windows. Toca "Instalar mi impresora".';
    for (const n of j.nombres) if (!impresoras.some((x) => x.host === n)) impresoras.push({ tipo: 'usb', host: n, puerto: 0, puestos: [] });
    pintarImpresoras('lista');
  };
  document.getElementById('instalarUsb').onclick = async () => {
    const p = document.getElementById('prog'); const b = document.getElementById('instalarUsb');
    b.disabled = true; b.textContent = 'Instalando...'; p.textContent = 'Detectando la impresora (medio minuto)...';
    const j = await api('instalar-impresora', {});
    b.disabled = false; b.textContent = 'Instalar mi impresora (USB)';
    p.textContent = j.mensaje || (j.ok ? 'Impresora lista.' : 'No se pudo instalar.');
    for (const n of (j.nombres || [])) if (!impresoras.some((x) => x.host === n)) impresoras.push({ tipo: 'usb', host: n, puerto: 0, puestos: [] });
    pintarImpresoras('lista');
  };
  document.getElementById('refrescarPuestos').onclick = async () => {
    const j = await api('puestos', {});
    if (!j.error) { borrador.puestos = j.puestos; pintarImpresoras('lista'); }
  };
  document.getElementById('agregar').onclick = () => {
    const h = document.getElementById('manualHost').value.trim();
    const p = Number(document.getElementById('manualPuerto').value) || 9100;
    if (h) { impresoras.push({ tipo: 'tcp', host: h, puerto: p, puestos: [] }); pintarImpresoras('lista'); }
  };
  document.getElementById('guardar').onclick = async () => {
    const err2 = document.getElementById('err2');
    if (!impresoras.length) { err2.textContent = 'Agrega al menos una impresora.'; return; }
    // Aviso: si NINGUNA impresora quedó en un puesto de cocina (todas en Despacho),
    // los pedidos de mesa y las comandas de cocina NO van a imprimir. Se avisa claro
    // dentro de la pantalla (no un cuadro del navegador) y se deja seguir a propósito.
    const hayCocina = impresoras.some((i) => (i.puestos || []).some((p) => p !== '__despacho'));
    if (!hayCocina && !guardarForzado) {
      err2.innerHTML =
        '<div class="aviso-reserva" style="color:#FAFAF9">' +
        '<b style="color:#FBBF24">Ojo, falta la cocina.</b><br>' +
        'No le asignaste ninguna impresora a un puesto de cocina. Así, los pedidos de <b>mesa</b> ' +
        'y las comandas de cocina <b>no van a imprimir</b>; solo saldrán los tickets de delivery y para llevar.' +
        '<div style="margin-top:8px;font-size:12.5px;color:#8A8FAF">Si tu negocio no tiene puestos de cocina, ' +
        'créalos en el panel (Configuración → Comandas por puesto) y toca "Actualizar puestos".</div>' +
        '<div class="fila" style="margin-top:12px">' +
        '<button id="volverAsignar" class="sec">Volver y asignar cocina</button>' +
        '<button id="igualGuardar">Guardar así (solo delivery)</button></div></div>';
      document.getElementById('volverAsignar').onclick = () => { err2.innerHTML = ''; };
      document.getElementById('igualGuardar').onclick = () => { guardarForzado = true; document.getElementById('guardar').click(); };
      return;
    }
    const b = document.getElementById('guardar'); b.disabled = true; b.textContent = 'Guardando...';
    const j = await api('guardar', { impresoras });
    if (j.error) { b.disabled = false; b.textContent = 'Guardar y arrancar'; err2.textContent = j.error; guardarForzado = false; return; }
    cargar();
  };
}

// La Estacion Fiscal del negocio: conectarla (pegando su llave de maquina) o
// ver como va (conectada, ventas por sellar, ultimo documento).
function tarjetaFiscal() {
  const f = E.extras && E.extras.fiscal;
  if (!f) return '';
  let cuerpo;
  if (f.configurada) {
    cuerpo =
      '<p class="ayuda">' +
      (f.conectada === false
        ? '<span class="pill r">no responde</span> La Estación no contesta. ¿Está abierta? Las ventas quedan en cola aquí y también viajan por la nube.'
        : '<span class="pill v">conectada</span> ' + esc(f.negocio || 'Estación Fiscal') + (f.rif ? ' · ' + esc(f.rif) : '') + (f.serial ? ' · ' + esc(f.serial) : '')) +
      '</p>' +
      '<p class="ayuda">Ventas por sellar: <b style="color:#FAFAF9">' + f.porSellar + '</b>' +
      (f.ultimoSello ? ' · Último documento: <b style="color:#FAFAF9">' + esc(f.ultimoSello) + '</b>' : '') + '</p>' +
      (f.problema ? '<p class="error" style="margin-top:4px">' + esc(f.problema) + '</p>' : '') +
      '<div class="fila" style="margin-top:8px"><button class="sec" id="fisQuitar">Desconectar</button><span id="resFis" class="error"></span></div>';
  } else {
    cuerpo =
      '<p class="ayuda">Si este negocio tiene la <b>Estación Fiscal</b> instalada en esta computadora, pega aquí su llave de máquina (sale en la pantalla de la Estación, empieza por <b>efm_</b>). Cada venta cobrada en la caja se le entrega de una, con o sin internet.</p>' +
      '<div class="fila"><input id="fisUrl" value="http://127.0.0.1:8739" style="flex:1;min-width:170px">' +
      '<input id="fisLlave" placeholder="efm_..." style="flex:2;min-width:200px">' +
      '<button id="fisConectar">Conectar</button></div><div id="resFis" class="error"></div>';
  }
  return '<div class="tarjeta"><h2 style="margin-top:0">Estación Fiscal</h2>' + cuerpo + '</div>';
}

function vistaEstado() {
  const m = E.motor;
  // Tablero a lo ancho: barra superior con la marca, el negocio y el estado.
  app.className = 'contenido amplia';
  barraSuperior(
    E.restaurante || 'Negocio',
    (E.enReserva ? '<span class="pill a">EN RESERVA</span>' : (m && m.canalVivo ? '<span class="pill v">EN VIVO</span>' : '<span class="pill r">RECONECTANDO</span>')) +
    (E.extras && E.extras.version ? '<span class="sub" style="font-family:Consolas,monospace">v' + esc(E.extras.version.actual) + '</span>' : ''),
  );
  app.innerHTML =
    (E.extras && E.extras.version && E.extras.version.disponible
      ? '<div class="aviso-reserva ancho"><b>Versión nueva disponible: ' + esc(E.extras.version.disponible) + '</b> (tienes la ' + esc(E.extras.version.actual) + '). ' +
        'Con un toque se descarga, se reemplaza y vuelve a arrancar solo.<div style="margin-top:10px"><button id="actualizar">' +
        (E.extras.version.descargando ? 'Descargando...' : 'Actualizar ahora') + '</button></div><div id="resAct" class="ok"></div></div>'
      : '') +
    '<div class="tarjeta">' +
    '<div class="fila" style="justify-content:space-between"><span class="grande">Comandas de la nube</span>' +
    '<span class="fila">' +
    (m && m.cola && m.cola.pendientes > 0 ? '<span class="pill a">' + m.cola.pendientes + ' esperando impresora</span>' : '') +
    (m && m.cola && m.cola.fallidas > 0 ? '<span class="pill r">' + m.cola.fallidas + ' sin imprimir</span>' : '') +
    '</span></div>' +
    (E.enReserva
      ? '<div class="aviso-reserva">Esta laptop está de <b>suplente</b>: no imprime. Si la titular se dañó, toca "Asumir el mando".</div>' +
        '<div style="margin-top:12px"><button id="mando">Asumir el mando</button></div>'
      : '<p class="ayuda" style="margin-top:8px">Las comandas de la nube se imprimen solas. Deja esta laptop prendida durante el servicio.</p>' +
        '<div class="fila" style="margin-top:10px">' +
        '<button id="reimprimir">Reimprimir última comanda</button>' +
        (m && m.cola && m.cola.fallidas > 0 ? '<button id="reintentar">Reintentar las que no salieron</button>' : '') +
        '<button class="sec" id="reserva">Poner en reserva</button></div>' +
        '<div id="resAcc" class="ok"></div>') +
    '</div>' +
    '<div class="tarjeta"><h2 style="margin-top:0">Punto de venta (tablets)</h2>' +
    (E.ip
      ? '<p class="ayuda">Funciona sin internet. En cada tablet, abre en el navegador: <b style="color:#FBBF24;font-size:15px">http://' + esc(E.ip) + ':' + (E.extras ? E.extras.puertoPos : 8738) + '</b></p>'
      : '<p class="ayuda">Funciona sin internet. Conecta la laptop a la red del local para ver la dirección de las tablets.</p>') +
    (E.extras && E.extras.salon
      ? '<p class="ayuda">Mesas abiertas: <b style="color:#FAFAF9">' + E.extras.salon.mesasAbiertas + '</b> · Ventas por subir a la nube: <b style="color:#FAFAF9">' + E.extras.salon.ventasPorSubir + '</b> · Nube: ' + (E.extras.nube && E.extras.nube.nubeViva ? '<span class="pill v">conectada</span>' : '<span class="pill a">sin internet (el salón sigue)</span>') + '</p>'
      : '') +
    '<div class="fila" style="margin-top:8px"><input id="pin" inputmode="numeric" placeholder="' + (E.extras && E.extras.pinListo ? 'Cambiar el PIN de sala' : 'Fijar el PIN de sala (6 a 8 números)') + '" style="flex:1;min-width:200px">' +
    '<button class="sec" id="ponPin">' + (E.extras && E.extras.pinListo ? 'Cambiar' : 'Fijar PIN') + '</button><span id="resPin" class="ok"></span></div></div>' +
    tarjetaFiscal() +
    '<div class="tarjeta"><h2 style="margin-top:0">Impresoras</h2><div id="lista"></div></div>' +
    '<div class="tarjeta"><div class="fila" style="justify-content:space-between"><b>Arrancar con Windows</b>' +
    '<button class="sec" id="auto">' + (E.autoarranque ? 'Quitar' : 'Activar') + '</button></div>' +
    '<p class="ayuda" style="margin-top:6px">' + (E.autoarranque ? 'Activado: el cerebrito arranca solo (oculto) al prender la laptop.' : 'Actívalo para que arranque solo al prender la laptop.') + '</p></div>' +
    '<div class="tarjeta ancho"><h2 style="margin-top:0">Actividad</h2><div class="log">' + esc((E.logs || []).join('\\n')) + '</div>' +
    '<div class="fila" style="margin-top:10px"><button id="reconfig">Cambiar configuración</button></div></div>';
  impresoras = agruparImpresoras(E.impresoras, E.despacho);
  pintarImpresoras('lista');
  // En estado la lista es de solo lectura (los cambios se hacen en "Cambiar configuración").
  document.querySelectorAll('#lista .checks input, #lista [data-quitar]').forEach((el) => { el.disabled = true; });
  const bR = document.getElementById('reimprimir');
  if (bR) bR.onclick = async () => { const j = await api('motor', { accion: 'reimprimir' }); document.getElementById('resAcc').textContent = j.codigo ? 'Reimpresa la #' + j.codigo : 'Aún no hay comandas impresas hoy.'; };
  const bRe = document.getElementById('reintentar');
  if (bRe) bRe.onclick = async () => { const j = await api('motor', { accion: 'reintentar' }); document.getElementById('resAcc').textContent = 'Reintentando ' + j.n + ' papel(es)...'; };
  const bRes = document.getElementById('reserva');
  if (bRes) bRes.onclick = async () => { await api('motor', { accion: 'reserva' }); cargar(); };
  const bM = document.getElementById('mando');
  if (bM) bM.onclick = async () => { await api('motor', { accion: 'mando' }); cargar(); };
  document.getElementById('auto').onclick = async () => { await api('autoarranque', { activar: !E.autoarranque }); cargar(); };
  const bP = document.getElementById('ponPin');
  if (bP) bP.onclick = async () => {
    const j = await api('pin', { pin: document.getElementById('pin').value.trim() });
    document.getElementById('resPin').textContent = j.error ? j.error : 'PIN guardado.';
  };
  const bFC = document.getElementById('fisConectar');
  if (bFC) bFC.onclick = async () => {
    bFC.disabled = true; bFC.textContent = 'Probando...';
    const j = await api('estacion-fiscal', { accion: 'conectar', url: document.getElementById('fisUrl').value.trim(), llave: document.getElementById('fisLlave').value.trim() });
    if (j.error) { document.getElementById('resFis').textContent = j.error; bFC.disabled = false; bFC.textContent = 'Conectar'; return; }
    cargar();
  };
  const bFQ = document.getElementById('fisQuitar');
  if (bFQ) bFQ.onclick = async () => { await api('estacion-fiscal', { accion: 'quitar' }); cargar(); };
  const bAct = document.getElementById('actualizar');
  if (bAct) bAct.onclick = async () => {
    bAct.disabled = true; bAct.textContent = 'Descargando...';
    const j = await api('actualizar', {});
    if (j.error) { document.getElementById('resAct').textContent = j.error; bAct.disabled = false; bAct.textContent = 'Actualizar ahora'; return; }
    document.getElementById('resAct').textContent = 'Aplicando: el cerebrito se reinicia solo, esta pagina vuelve en un momento...';
    setInterval(async () => { try { const r = await fetch('/api/estado'); if (r.ok) location.reload(); } catch(e){} }, 4000);
  };
  document.getElementById('reconfig').onclick = async () => {
    const b = document.getElementById('reconfig'); b.disabled = true; b.textContent = 'Abriendo...';
    const j = await api('reconfigurar', {});
    if (j.error) {
      b.disabled = false; b.textContent = 'Cambiar configuración';
      if (j.reentrar) { vistaAsistente(); const e1 = document.getElementById('err1'); if (e1) e1.textContent = j.error; return; }
      const acc = document.getElementById('resAcc');
      if (acc) acc.textContent = j.error; else b.textContent = j.error;
      return;
    }
    borrador = j; impresoras = []; vistaImpresoras();
  };
}

async function cargar() {
  E = await api('estado');
  if (!E.configurado) vistaAsistente();
  else vistaEstado();
}
cargar();
setInterval(async () => { if (E && E.configurado) { E = await api('estado'); if (E.configurado) vistaEstado(); } }, 15000);
</script></body></html>`;

// ============================ el servidor ==================================
export function iniciarAsistente(ctx: Contexto): void {
  let borrador: Borrador | null = null;
  let ultimaVerificacion = 0;

  // Auto-reparacion: versiones hasta la 2.6.8 escribieron arrancar_oculto.vbs con
  // escapes JSON (invalidos en VBScript); Windows mostraba un error fatal en cada
  // inicio de sesion y el cerebrito no arrancaba solo. Si el autoarranque esta
  // activo, se reescribe el .vbs correcto al arrancar (idempotente).
  // SOLO el exe instalado: en modo desarrollo/pruebas (node dist) esto apuntaria
  // el autoarranque real de la maquina a la carpeta de trabajo.
  void (async () => {
    try {
      if (ctx.esExe && (await autoarranqueActivo())) await ponerAutoarranque(ctx, true);
      // El icono del escritorio (ventana de aplicacion, sin barra de
      // direcciones) se asegura en cada arranque: una version nueva del icono
      // pisa la vieja. Solo el exe instalado.
      if (ctx.esExe) await accesoDirectoApp(ctx.base, PUERTO_ASISTENTE);
    } catch {
      ctx.log('No se pudo revisar el .vbs del autoarranque al arrancar.');
    }
  })();

  const server = createServer((req, res) => {
    const responder = (obj: unknown, status = 200) => {
      res.writeHead(status, { 'Content-Type': 'application/json' });
      res.end(JSON.stringify(obj));
    };
    const url = req.url ?? '/';

    if (req.method === 'GET' && (url === '/' || url === '/index.html')) {
      res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
      res.end(PAGINA);
      return;
    }

    // La cara de aplicacion: con esto la ventana en modo app (--app) sale con
    // su nombre, su icono y su color.
    if (req.method === 'GET' && url === '/manifest.webmanifest') {
      res.writeHead(200, { 'Content-Type': 'application/manifest+json', 'cache-control': 'no-store' });
      res.end(JSON.stringify({
        name: 'Cerebrito Gustito',
        short_name: 'Cerebrito',
        start_url: '/',
        display: 'standalone',
        background_color: '#0B0D14',
        theme_color: '#0B0D14',
        icons: [{ src: '/icono.svg', sizes: 'any', type: 'image/svg+xml' }],
      }));
      return;
    }
    if (req.method === 'GET' && url === '/icono.svg') {
      res.writeHead(200, { 'Content-Type': 'image/svg+xml', 'cache-control': 'max-age=86400' });
      res.end(`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
<rect width="64" height="64" rx="14" fill="#0B0D14"/>
<rect x="6" y="6" width="52" height="52" rx="10" fill="none" stroke="#FBBF24" stroke-width="4"/>
<text x="32" y="42" text-anchor="middle" font-family="Segoe UI, sans-serif" font-size="24" font-weight="800" fill="#FBBF24">CG</text>
</svg>`);
      return;
    }

    if (req.method === 'GET' && url === '/api/estado') {
      // Revisar version al abrir/refrescar la pagina (freno de 5 min): antes solo
      // se revisaba al arrancar y cada 12h, y el aviso de actualizar no salia.
      if (Date.now() - ultimaVerificacion > 300_000) {
        ultimaVerificacion = Date.now();
        ctx.verificarActualizacion?.();
      }
      void (async () => {
        const cfg = ctx.obtenerConfig();
        const m = ctx.motor();
        responder({
          configurado: !!cfg,
          enReserva: !!cfg?.enReserva,
          restaurante: m?.nombreRestaurante ?? borrador?.restaurante ?? null,
          motor: m ? m.estado() : null,
          impresoras: cfg?.impresoras ?? [],
          despacho: cfg?.despacho ?? null,
          puestos: m && typeof m.puestos === 'function' ? m.puestos() : (borrador?.puestos ?? []),
          autoarranque: await autoarranqueActivo(),
          logs: ctx.logs().slice(-60),
          extras: ctx.extras(),
          ip: ipLocal(),
        });
      })();
      return;
    }

    if (req.method !== 'POST' || !url.startsWith('/api/')) {
      responder({ error: 'No existe' }, 404);
      return;
    }

    let cuerpo = '';
    req.on('data', (c) => (cuerpo += c));
    req.on('end', () => {
      void (async () => {
        let datos: Record<string, unknown> = {};
        try {
          datos = cuerpo ? (JSON.parse(cuerpo) as Record<string, unknown>) : {};
        } catch {
          /* cuerpo vacio */
        }
        try {
          if (url === '/api/entrar') {
            const r = await loginNegocio(ctx, String(datos.correo ?? ''), String(datos.clave ?? ''));
            if ('error' in r) return responder(r, 400);
            borrador = r;
            return responder({ restaurante: r.restaurante, puestos: r.puestos, configNube: r.configNube });
          }

          if (url === '/api/puestos') {
            if (!borrador) return responder({ error: 'Entra primero con tu cuenta.' }, 400);
            const r = await loginNegocio(ctx, borrador.correo, borrador.clave);
            if ('error' in r) return responder(r, 400);
            borrador = r;
            return responder({ puestos: r.puestos, configNube: r.configNube });
          }

          if (url === '/api/reconfigurar') {
            // Cambiar la configuracion NO es cerrar sesion: el cerebrito entra
            // solo con la cuenta que ya tiene guardada y va DIRECTO al paso de
            // impresoras. Solo si esa clave ya no sirve (la cambiaron en la
            // nube) se vuelve a pedir la cuenta.
            const cfg = ctx.obtenerConfig();
            if (!cfg) return responder({ error: 'Configura primero el cerebrito.' }, 400);
            let r;
            try {
              r = await loginNegocio(ctx, cfg.correo, cfg.clave);
            } catch {
              return responder({ error: 'Sin internet no se puede cambiar la configuracion: la cuenta se comprueba en la nube.' }, 503);
            }
            if ('error' in r) {
              return responder({ error: 'La cuenta guardada ya no entra (¿cambiaron la clave del negocio?). Entra de nuevo.', reentrar: true }, 401);
            }
            borrador = r;
            return responder({ restaurante: r.restaurante, puestos: r.puestos, configNube: r.configNube });
          }

          if (url === '/api/escanear') {
            ctx.log('Buscando impresoras en la red...');
            const ips = await buscarImpresoras(9100);
            ctx.log(`Escaneo listo: ${ips.length} impresora(s)`);
            return responder({ ips });
          }

          if (url === '/api/usb') {
            ctx.log('Buscando impresoras USB (colas de Windows)...');
            const nombres = await listarImpresorasUsb();
            ctx.log(`Impresoras USB: ${nombres.length}`);
            return responder({ nombres });
          }

          if (url === '/api/instalar-impresora') {
            ctx.log('Detectando e instalando impresora automaticamente...');
            const r = await instalarImpresoraAuto();
            ctx.log(`Instalacion automatica: ${r.ok ? (r.nombre ?? 'ok') : 'no'} · ${r.mensaje}`);
            const nombres = r.ok ? await listarImpresorasUsb() : [];
            return responder({ ...r, nombres });
          }

          if (url === '/api/probar') {
            const host = String(datos.host ?? '');
            const puerto = Number(datos.puerto ?? 9100);
            const esUsb = datos.tipo === 'usb';
            try {
              if (esUsb) await enviarUsb(host, comandaABytes(ticketPrueba(`USB ${host}`)));
              else await enviarTcp(host, puerto, comandaABytes(ticketPrueba(`${host}:${puerto}`)), 4000);
              ctx.log(`Prueba de impresion OK en ${esUsb ? `USB "${host}"` : `${host}:${puerto}`}`);
              return responder({ ok: true });
            } catch (e) {
              return responder({ ok: false, error: (e as Error).message });
            }
          }

          if (url === '/api/guardar') {
            if (!borrador) return responder({ error: 'Entra primero con tu cuenta.' }, 400);
            const lista = (datos.impresoras ?? []) as { host: string; puerto: number; tipo?: string; puestos?: string[] }[];
            if (!lista.length) return responder({ error: 'Agrega al menos una impresora.' }, 400);
            // Cada impresora fisica sale con VARIAS entradas de config, una por
            // puesto marcado. '__despacho' -> puesto 'Despacho' (ticket de respaldo).
            const impresoras: ImpresoraCfg[] = [];
            let hayDespacho = false;
            for (const i of lista) {
              const tipo = i.tipo === 'usb' ? 'usb' : 'tcp';
              const puerto = tipo === 'usb' ? 0 : Number(i.puerto) || 9100;
              for (const p of i.puestos ?? []) {
                if (p === DESPACHO) { hayDespacho = true; impresoras.push({ puesto: 'Despacho', tipo, host: i.host, puerto }); }
                else impresoras.push({ puesto: p, tipo, host: i.host, puerto });
              }
            }
            if (!impresoras.length) {
              return responder({ error: 'Marca al menos un puesto en cada impresora (qué imprime cada máquina).' }, 400);
            }
            // Preservar despacho/sinPuesto/pin de la config previa (local) o de la
            // nube: asi ajustes como "respaldo del negocio en mesa" (despacho.tipos)
            // NO se pierden cuando se re-guarda desde el asistente ni al montar una
            // laptop nueva que baja la config de la nube.
            const prev = ctx.obtenerConfig();
            // El ticket de respaldo/despacho sale por la impresora con puesto 'Despacho'
            // (la que marco la casilla). Conservamos los TIPOS (incluye 'mesa' = respaldo
            // del negocio) de la config previa o de la nube; sin herencia, delivery/pickup.
            const tiposDespacho = prev?.despacho?.tipos ?? borrador.configNube?.despacho?.tipos ?? ['delivery', 'pickup'];
            const despacho = hayDespacho ? { puesto: 'Despacho', tipos: tiposDespacho } : undefined;
            const sinPuesto = prev?.sinPuesto ?? borrador.configNube?.sinPuesto ?? 'defecto';
            const pinSala = prev?.pinSala ?? borrador.configNube?.pinSala;
            const cfg: Config = {
              supabaseUrl: ctx.urlSupabase(),
              anonKey: ctx.anonKey(),
              correo: borrador.correo,
              clave: borrador.clave,
              impresoras,
              sinPuesto,
              ...(despacho ? { despacho } : {}),
              ...(pinSala ? { pinSala } : {}),
              carpetaEstado: join(ctx.base, 'estado'),
              // Re-guardar la configuracion NO asume el mando: una suplente
              // que ajusta impresoras sigue siendo suplente (dos laptops EN
              // VIVO = comandas dobles). El mando se toma solo con su boton.
              enReserva: prev?.enReserva ?? false,
              // Y tampoco desconecta la Estacion Fiscal: la llave se preserva
              // igual que el despacho y el PIN (cambiar impresoras no tiene
              // por que tumbar la facturacion).
              ...(prev?.estacionFiscal ? { estacionFiscal: prev.estacionFiscal } : {}),
            };
            ctx.guardarConfig(cfg);
            // Subir a la nube (best-effort): la proxima laptop con esta cuenta baja
            // el mapeo y los ajustes solos. NO se suben correo/clave/token.
            void guardarConfigNube(ctx.urlSupabase(), ctx.anonKey(), borrador.token, borrador.restauranteId, {
              impresoras,
              ...(despacho ? { despacho } : {}),
              sinPuesto,
              ...(pinSala ? { pinSala } : {}),
            }).then((ok) => ctx.log(ok ? 'Config subida a la nube (bajara en otras laptops).' : 'No se pudo subir la config a la nube (se guardo local).'));
            if (ctx.esExe) void accesoDirectoApp(ctx.base, PUERTO_ASISTENTE); // solo en la laptop del cliente
            const err = await ctx.reiniciarMotor();
            if (err) return responder({ error: err }, 400);
            borrador = null;
            return responder({ ok: true });
          }

          if (url === '/api/motor') {
            const accion = String(datos.accion ?? '');
            const cfg = ctx.obtenerConfig();
            if (!cfg) return responder({ error: 'Sin configuracion' }, 400);
            if (accion === 'reimprimir') {
              const codigo = ctx.motor()?.reimprimirUltima() ?? null;
              return responder({ codigo });
            }
            if (accion === 'reintentar') {
              const n = ctx.motor()?.reintentarFallidas() ?? 0;
              ctx.log(`Reintentando ${n} papel(es) fallido(s).`);
              return responder({ n });
            }
            if (accion === 'reserva' || accion === 'mando') {
              cfg.enReserva = accion === 'reserva';
              ctx.guardarConfig(cfg);
              const err = await ctx.reiniciarMotor();
              if (err) return responder({ error: err }, 400);
              return responder({ ok: true });
            }
            return responder({ error: 'Accion invalida' }, 400);
          }

          if (url === '/api/autoarranque') {
            await ponerAutoarranque(ctx, !!datos.activar);
            return responder({ ok: true });
          }

          if (url === '/api/actualizar') {
            const err = await ctx.aplicarActualizacion();
            return responder(err ? { error: err } : { ok: true });
          }

          if (url === '/api/estacion-fiscal') {
            const cfg = ctx.obtenerConfig();
            if (!cfg) return responder({ error: 'Configura primero el cerebrito.' }, 400);
            if (datos.accion === 'quitar') {
              delete cfg.estacionFiscal;
              ctx.guardarConfig(cfg);
              ctx.log('Estacion Fiscal desconectada de esta laptop.');
              return responder({ ok: true });
            }
            const llave = String(datos.llave ?? '').trim();
            const direccion = String(datos.url ?? '').trim() || 'http://127.0.0.1:8739';
            if (!llave.startsWith('efm_')) {
              return responder({ error: 'Esa no parece la llave de máquina (empieza por efm_). Sale en la pantalla de la Estación.' }, 400);
            }
            cfg.estacionFiscal = { url: direccion, llave };
            ctx.guardarConfig(cfg);
            const r = await ctx.probarEstacionFiscal();
            if (!r.ok) {
              // Si la llave o la direccion estan malas, no se dejan guardadas:
              // un puente mal conectado martillando cada 20s no ayuda a nadie.
              delete cfg.estacionFiscal;
              ctx.guardarConfig(cfg);
              return responder({ error: r.problema ?? 'La Estación no respondió. ¿Está abierta en esta computadora?' }, 400);
            }
            ctx.log(`Estacion Fiscal conectada: ${r.negocio ?? ''} (${r.rif ?? ''}). Cada venta cobrada se le entrega directo.`);
            ctx.dispararPuenteFiscal();
            return responder({ ok: true, negocio: r.negocio, rif: r.rif });
          }

          if (url === '/api/pin') {
            const cfg = ctx.obtenerConfig();
            if (!cfg) return responder({ error: 'Configura primero el cerebrito.' }, 400);
            const pin = String(datos.pin ?? '').trim();
            // Seis numeros minimo (antes cuatro): con el freno de la puerta
            // del POS, seis vuelve inviable adivinarlo. Auditoria 2026-07-30.
            if (!/^\d{6,8}$/.test(pin)) return responder({ error: 'El PIN debe tener de 6 a 8 numeros.' }, 400);
            cfg.pinSala = pin;
            ctx.guardarConfig(cfg);
            ctx.log('PIN de sala actualizado.');
            return responder({ ok: true });
          }

          responder({ error: 'No existe' }, 404);
        } catch (e) {
          responder({ error: 'Error inesperado: ' + (e as Error).message }, 500);
        }
      })();
    });
  });

  // SOLO la propia laptop puede configurar (la clave del negocio viaja por aqui).
  server.listen(PUERTO_ASISTENTE, '127.0.0.1', () => {
    ctx.log(`Asistente listo en http://127.0.0.1:${PUERTO_ASISTENTE}`);
  });
  server.on('error', (e) => ctx.log('Asistente no pudo arrancar: ' + e.message));
}
